AI-powered automatic replies to Wildberries customer reviews

A module that picks up new customer reviews on its own, writes replies to them — good ones are closed with ready-made templates, complaints are answered by a language model — and sends the reply back to Wildberries. Contentious reviews that need a human are set aside and not sent.

DESCRIPTION

Technically this is part of the same Wildberries analytics project, but it is a separate circuit: not data collection but working with customers, and it is described here as a standalone engineering task.

The store receives around three hundred reviews a day, and replying to each one is manual work that used to be done in fits and starts, with a delay of a day or two. The module is embedded into the general parser cycle, which spins every 20–30 minutes, so a review that arrives in the morning gets a reply within half an hour. The circuit consists of three scripts — collecting reviews from the Wildberries API, preparing a reply, sending the reply — and a shared PostgreSQL table through which they are connected.

Collecting reviews

Collection is built around the platform's limits. The store has about a million and a half reviews in total, while a Wildberries query physically will not return more than two hundred thousand records, and re-reading answered and archived ones is pointless — they do not change. So only unanswered reviews and only for the last 24 hours are requested: one or two requests per cycle instead of four dozen. The Wildberries limiter is punitive — every attempt to reach it while access is closed extends the block (we observed the backoff growing from 540 to 654 seconds) — so on a rate-limit refusal the script does not retry and does not wait, it exits and tries again on the next cycle. The review text is assembled from three WB fields: customers often write their complaint only in "cons", leaving the main text empty.

The flip side of the "unanswered only" query is that we stop seeing a review that was answered by hand in the dashboard or that moved to the archive. This is handled on the database side: a review that was in our processing but did not appear in the fresh query is automatically taken off processing. The reconciliation runs strictly within the query window — by the date of the oldest review in it — otherwise everything the script simply did not request would be marked answered for no reason.

State model

The reviews table works as a state machine: new (awaiting a reply) → ready (reply prepared) or check (reply prepared but a human must read it) → sent, plus the terminal answered for reviews that went out without us. The separation of columns is fundamental: the collection script updates only what comes from WB, and our columns are not part of its upsert — otherwise the next cycle would overwrite a finished reply and we would pay for the same review twice. The processing queue is exactly the new status: as soon as a reply is written, the review leaves the queue forever, no matter how many times the script is run.

Two streams for preparing replies

Replies are prepared in two streams, split by rating. Reviews with 4–5 stars get a ready-made template from an external file in rotation — the model is not called here at all, and this is free; the position in the template list is not stored in a separate counter but computed from the database itself, so the order continues between runs and the same text does not go out twice in a row. Reviews with 1–3 stars and reviews without a rating go to the language model: thanking someone for their purchase when they are in fact unhappy is worse than calling the model one more time.

Economics: the Batch API

The economics are solved through the Batch API: it halves the price regardless of batch size, so the flow can be broken into small frequent batches — there is no need to accumulate a day's worth for the discount. This works out to about $0.001 per review, roughly three dollars a month. A separate piece of work is not paying twice. Waiting for a batch is capped at fifteen minutes instead of the day the provider allows: the script sits inside the general cycle and cannot wait for hours. If a batch does not finish in time, the reviews stay in the queue, and the next run first checks whether a batch from the previous cycle is still being computed and waits for it instead of sending a new one. The request identifier inside a batch is the review identifier itself, not a sequence number: a positional key would tie the answers to the order of the list, and after a failure there would be nothing left to match already-paid answers to reviews, while a mismatch means someone else's reply under someone else's review. The cost of each run is calculated from actual tokens and written to the log.

Structured model output

The model answers not with text but with a structure defined by a JSON schema: the finished reply, an "this is for a human" flag, and the reasoning behind the flag. The reasoning is not written to the database, but it was deliberately left in the schema — with a mandatory explanation the model makes noticeably more stable decisions. All texts for the model are moved into separate markdown files: instructions on tone and a walkthrough of typical situations (defects, wrong item, wrong size, money), output rules, and the list of templates. They are edited by someone who is not a programmer, and they are edited without changing the code.

The line between "we answer" and "call a human"

The line between answering ourselves and calling a human is described by an explicit list of markers in the review itself, not by the model's "confidence": a legal threat, harm to health or property, a demand for documents and certificates, an accusation of counterfeit, a monetary demand, profanity and insults, and the case where an honest reply would require promising a refund or admitting fault. A harsh tone, caps lock and general irritation are not grounds — otherwise half the flow would go to review. A reply is written in every case, even when the review goes to a human: the human is left to read a ready text rather than compose one from scratch.

Prompt caching economics

Instruction caching was worked through as well: on the chosen model the cache kicks in from 4,096 tokens, while the instruction with the rules takes about 1,250 — that is, it silently does not work, without any error. The script counts the instruction tokens itself (counting is free) and warns about this in the log; moving to a more expensive model for the sake of the cache would not pay off, and as soon as the instruction grows to the threshold the cache will switch on by itself. The cache check must not be able to bring down a run: previously its failure on the very first API request left the whole cycle without replies, now it is silently skipped.

Access to the model from a closed server

Access was solved separately: api.anthropic.com is blocked on the server, while Wildberries, Google and the database are reachable directly. Global environment variables are no good here — they would push all of the project's traffic through the proxy. The proxy is attached to the single http client created for requests to the model, while the rest of the project keeps going directly; the isolation was verified to work in both directions. Connection timeouts and retry counts were also raised under the proxy: with default settings the connection timed out thirteen cycles in a row.

Sending replies

Sending replies is moved into a separate script and lives together with the other outbound exports. Only reviews in the ready status are taken — those flagged for review are not touched until a human moves them along. Sending is irreversible: the customer sees the reply on the product card and it cannot be recalled, so a limited number of replies goes out per run, an error on a specific review leaves it in the queue for the next cycle, and a rate-limit refusal stops the whole run rather than provoking the limiter further.

Where it runs

One more decision is where it runs. A batch takes minutes, while the child scripts of the general dispatcher run in parallel with an individual limit of 200 seconds and would be killed halfway. So reply preparation is called directly from the root process, and error interception into the shared log is placed inside the script itself. The database connection is not held open while the batch is being computed: over minutes of waiting the database drops an idle connection, and already-paid answers would be lost on write.

TOOLS USED

  • Python 3
  • Anthropic API
  • Claude Haiku 4.5
  • Batch API
  • structured output (JSON schema)
  • prompt caching
  • token counting
  • REST API Wildberries (feedbacks-api)
  • PostgreSQL
  • psycopg2
  • requests
  • httpx
  • SOCKS5 proxy
  • Markdown prompts
  • Git

RESULT

  • A reply to a review is prepared within half an hour instead of manual work every day or two; around three hundred reviews a day are processed without human involvement.
  • The cost was brought down to ~$0.001 per review (about $3 a month) thanks to the Batch API and splitting the flow: 4–5 stars get templates, the model is only called for complaints.
  • Protection against paying twice: a status queue, columns that are not overwritten on updates from WB, a search for an unclosed batch from the previous cycle, and matching answers by review identifier rather than by position in the list.
  • Collection is built around the platform's limits: only unanswered reviews from the last 24 hours (one request instead of forty), correct handling of the punitive limiter, and automatic removal from processing of reviews answered by hand or moved to the archive.
  • The model answers with a structure defined by a JSON schema; escalation rules are described by an explicit list of markers (legal threat, harm to health, demand for documents, counterfeit, monetary demand, profanity) rather than by the model's "confidence".
  • All instructions, rules and templates are moved into markdown files and edited without changing the code.
  • The prompt-cache economics were worked out: the caching threshold is checked on the fly and the script warns when the cache silently fails to switch on.
  • Access to the model from a closed server was solved with a targeted proxy on a single http client, without affecting the rest of the project's traffic.
  • Sending was made irreversibly safe: a per-run limit, error isolation per review, stopping the run on a rate-limit refusal, and a "for human review" status for reviews with legal or medical risk.

AI-ASSISTANT

Ask a question about this project