← Back to experience
Project write-up · April 2026 – present

店小力 · Dianxiaoli
An AI shop assistant that lives inside WeChat and WhatsApp

A single source of truth for a small merchant — prices, stock, policies, orders — that buyers, staff and the owner all query through the chat app they already use. Built and operated solo; in production with three paying merchants on two continents.

Live in production 3 paying merchants · China & South Africa Solo-built since April 2026 Python · FastAPI · Railway DeepSeek · Anthropic · OpenAI

The problem

In a small trading business the workflow is the people. The owner sets prices, the buyer sets quotes, the technician answers after-sales. The business runs when they are online and stops when they are not, and knowledge leaves with whoever leaves. The most visible symptom is on the customer side: a wholesale stall in Johannesburg measured roughly 40% of buyer inquiries going unanswered over the 30 days before deployment, because five people were already in a hundred WhatsApp threads.

Dianxiaoli moves those facts out of people's heads and into one place, then lets three kinds of user ask it — each seeing only the fields they should.

BuyerAsks for a price, negotiates, sends a payment screenshot, asks about shipping. Gets an answer in their language in seconds, from the merchant's own catalogue.
StaffAsks the same question a buyer would — and gets the fields a salesperson is allowed to see (the authorised floor, never cost or another buyer's price), with the time the price was last confirmed. Can propose a price change for the owner to approve, and raise an enquiry the owner answers once for everyone.
OwnerApproves orders and payments, updates prices, adds products, reads a daily report — in one-line plain-language messages.

What is live

Each merchant runs on WeChat Customer Service or WhatsApp Business through the platforms' official APIs. Deployment for the first merchant took three weeks; it is now two to three days, using an industry-pack layer (wholesale stall / retail / factory / service) and 14 pluggable conversation skill packs on a shared core — the owner switches packs with one message, and every default can be overridden individually.

How a message moves through the system

One FastAPI service serving many merchants, each request routed to its store by the channel account it arrived on (multi-tenant since September 2026). Everything below is deterministic Python around exactly one model call per turn; the model produces text and a small set of tagged actions, and the service decides what to do with them.

Webhook in WeChat KF · WhatsApp Normalise + role buyer · staff · owner Media pre-pass Whisper · vision + conf. floor Catalogue lookup exact SKU / keyword table brain() DeepSeek · one call per turn reply + action tags text matched rows query Guards every number ∈ catalogue red lines · demo toggle Rule engine quote · order · escalate if any provider is down timeout Execute actions pending order · proof ledger state → persistent volume Reply to buyer P95 < 3 s Owner notify queue · daemon thread 18/min · WeCom · webhook push() returns at once Owner brain approve · reprice · add own prompt + permissions owner replies Background loop · every 5 min follow-up: unpaid pending > 120 min → one nudge (write-before-send) daily 9-item audit of yesterday's conversations → owner report
Request path for one buyer turn. Solid boxes are on the reply path; the dashed box is the fallback when a provider times out; the owner channel is a separate thread so it can never slow the buyer.

1 · Inbound and roles

Each channel's webhook is verified, then folded into one message envelope: merchant, channel, user, role, and one of text / image / voice. Role is resolved before anything else — the owner is a whitelisted user id, staff are users who joined with a one-time code the owner issued, everyone else is a buyer; the merchant's industry pack (wholesale stall / retail / factory / service) sets the defaults for the buyer branch, and factory mode adds FOB tiers, MOQ, lead times and PI drafts. The three paths never share a prompt: the staff path is fully deterministic (no model call — it reads the catalogue through a field-visibility table), and the owner brain has its own system prompt and its own set of permitted actions.

2 · Media pre-pass

Voice notes go through Whisper and re-enter as text. Images go through a vision model with a task-specific prompt and a confidence floor — below the floor the image is handed to the owner instead of interpreted. The classifier separates payment screenshots (→ pending payment proof), product photos (→ catalogue match), and spec sheets (→ factory quote). The vision provider is swappable by environment variable; a fallback model is configured.

3 · Retrieval

Catalogue lookup is a keyword and identifier table over the merchant's own product sheet, not an embedding index. The table is generated at import time from the merchant's Excel (each row becomes SKU, aliases, and a small keyword set), and matching is exact-first. A single-character keyword only fires when it sits next to an "asking for goods" signal, which stopped a class of false matches in Chinese. A miss returns a fixed "we don't carry that — here is what we do have" with the top three live items, so the model never has to improvise a product.

4 · One model call, tagged output

brain() assembles the prompt from merchant facts, matched catalogue rows, the last few turns, and the three red lines, then makes one call. The model returns natural-language text plus zero or more action tags — [ORDER:sku:qty], [WAITLIST], [ESCALATE]. The tags are the only way the model can cause a side effect; everything else it says is just text.

5 · Guards before send

The reply is parsed, not trusted. Every number in it is checked against the matched catalogue rows; a number that isn't there is a hallucinated price and the reply is regenerated or replaced with an escalation. Red-line filters run on the text (stock counts, other buyers' prices, floor prices). A "demo catalogue" toggle lets a merchant hide sample products without the model seeing them at all. If the provider times out, the same guarded path runs on a rule engine that can quote, take an order and escalate — degraded, never silent.

6 · Actions and state

The service executes the tags. An order becomes a pending item with an id; a payment screenshot becomes an append-only proof record (id / order / user / media / amount / status) that approval or rejection only flips the status of — nothing is deleted. Per-merchant state is a JSON document on a persistent volume, written atomically after each turn.

7 · Owner side, off the hot path

Owner notifications are enqueued and delivered by a daemon thread over WeCom application messages or a webhook, with a token cache, an 18-per-minute limiter, and a mapping from provider error codes to one-line fixes ("IP not whitelisted → add the egress IP"). If both channels fail, delivery falls back to the customer-service channel inside its 48-hour window. The call to enqueue returns in microseconds, which is why adding owner notifications measured 4–5 ms on the buyer path instead of the 300–800 ms a synchronous call would have added. The owner replies in plain language — "approve #12", "raise A100 to 480", "add product", "authorise the risk profile" — and a separate owner brain turns that into state changes.

8 · Background loops

A five-minute loop runs the follow-up check: any pending order with no proof after 120 minutes gets exactly one nudge to the buyer and one line to the owner. The "followed-up" flag is written to disk before the message is sent, so a buyer replying in the same second can't cause a double nudge. A daily job re-reads yesterday's conversations against a nine-item audit (red-line violations, unanswered messages, quoting errors, escalation latency) and reports to the owner; a weekly roll-up follows. The audit is itself tested by mutation: the suite deliberately breaks a rule in a copy of the pipeline — leaks a stock count, confirms a payment — and asserts the audit catches it on real conversation traffic.

9 · Release

Twenty-one suites, 1,458 checks. The scenario gates are the important ones: scripted buyer conversations in Chinese and in factory mode, each with the expected action tags and the numbers the reply must and must not contain. Vision, import, notification, proof-and-follow-up, consent, multi-tenancy, the staff role and workbench, aliases and invoicing, industry packs and cold-start each have their own suite; several of them start the previous release in a subprocess and compare buyer replies word for word, so a change to a default can't silently alter what an existing store says; an end-to-end run starts the real server against fake platform credentials and drives it with Playwright. A release that fails any of them doesn't ship — including two of mine.

Decisions I would defend

Catalogue retrieval is exact-match, not vector search — on purpose

Product names and SKUs are identifiers, not concepts. Semantic similarity happily matches A100 to A200, and a wrong-but-plausible price is the worst failure a quoting system can have. Retrieval is keyword and identifier matching over the merchant's table, with an explicit "we don't carry that — here is what we do have" path when nothing matches. The one place vector retrieval is used is the after-sales assistant, where the corpus is long-form FAQ text and the failure mode is benign.

Hard constraints live outside the model

Three rules the assistant cannot break regardless of prompt: it never states stock quantities, never reveals what another buyer paid, and never discloses or invents a floor price. Separately, the model never confirms a shipment and never judges whether money has arrived — a payment screenshot creates a pending item for the owner, who approves it; the reply to the buyer is generated only after that. These are enforced in code and tested, not requested in a prompt.

The buyer's reply path is protected from everything else

Owner notifications (new order, failed delivery, payment proof) go through a queued, asynchronous channel with rate limiting. Adding them did not change buyer latency — the reply path measures 4–5 ms of overhead before the model call, and end-to-end P95 stays under 3 s.

Merchant data leaves only with consent, through a separate key

A 17-field operating-health profile (inquiry volume, conversion, receivables structure, proof-verification rate, stock value) can be exported to a partner — but only after the owner grants consent in-chat, through a key that is distinct from the owner's own key, with grant/revoke idempotent and logged. Revoking returns 403 immediately.

Testing and evaluation

The system is released only through an automated gate: 21 test suites, 1,458 checks — buyer scenario gates (Chinese and factory-mode), vision, catalogue import, owner notifications, payment-proof and follow-up, consent, multi-tenancy, staff role and workbench, aliases and invoicing, industry packs, cold start, plus Playwright end-to-end runs against a live server. The gate has blocked my own releases twice. Several suites are golden tests: they check out the previous release with git archive, run it in a subprocess, and compare buyer replies word for word, so a new default cannot silently change what an existing store says. In production, a daily nine-item audit reviews the previous day's conversations for red-line violations, unanswered messages and quoting errors, and reports to the owner.

The thing I care about most is the same thing I learned building a handwriting-grading system earlier: the interesting failures are quiet ones. A quoting assistant that is right 95% of the time and confidently wrong 5% of the time is worse than one that says "let me check with the owner" — so the evaluation work is about finding where the model fails and moving those cases out of its hands.

1,458
automated checks across 21 suites, all green before release
< 3 s
P95 end-to-end reply latency in production
3 wk → 2–3 d
time to deploy a new merchant
≈ ¥100
model and hosting cost per store per month, measured

Status and what is next

The source is private while the collaboration agreement is being finalised; I am glad to walk through the code and test suites on a call.
Harrison He · harrisonhe.com · harrisonhe.01150@gmail.com