Deep dive into
liyupi/yu-ai-learn— a fully open-sourced "AI quiz/level-clear learning" WeChat mini-program built with Taro + FastAPI + LangGraph + DeepSeek — and how to rebuild the same product in an afternoon on NXagents.
A Chinese dev community post went around recently: "又一个 AI 新项目完结,用 DeepSeek 搞了个微信小程序!" (Another AI project finished — built a WeChat mini-program with DeepSeek!). It's 程序员鱼皮 (Programmer YuPi) announcing the completion of his AI 闯关学习小程序 (AI Quiz-Quest Learning Mini-Program) — a full course project that turns any knowledge into a game.
The premise is genius in its simplicity: learning is boring, games are not. So why not let AI convert whatever you want to learn into a level-based quiz game, complete with instant explanations, AI-generated review reports, XP points, and even AI-illustrated questions?
The whole thing is open source (MIT): github.com/liyupi/yu-ai-learn
In this post I'll:
This is not a toy CRUD app. It's a full AI agent product with 30+ features:
The killer insight: "This project's skeleton can be reused in any vertical — swap the knowledge source and you have a new product: driving-test prep, interview question banks, corporate training, children's English word games."
| Layer | Choice |
|---|---|
| Mini-program frontend | Taro 4 · React 18 · TypeScript · Sass |
| Backend | Python 3.11 · FastAPI · Pydantic v2 · Uvicorn · asyncio |
| AI orchestration | LangChain · LangGraph (create_react_agent) |
| LLMs / search / images | DeepSeek (quiz+report) · 阿里云百炼 (embeddings + image gen) · Tavily (web search) |
| Vector store | Chroma (per-user collections) |
| Data | MySQL (async pool) · Tencent COS (object storage) |
| Auth | WeChat jscode2session + JWT |
| Async | quiz_tasks table + background job + status polling |
| Tests | pytest + pytest-asyncio (139 tests!) |
| Deploy | Docker → WeChat Cloud Run |
I read through the source. Here are the patterns that separate a student project from a real product.
LLMs love wrapping JSON in markdown fences. This project wins by being paranoid:
def _extract_json(text: str) -> dict:
"""Extract JSON from LLM output, tolerant of ```json fences."""
match = re.search(r"```(?:json)?\s*([\s\S]*?)```", text)
raw = match.group(1).strip() if match else text.strip()
return json.loads(raw)
The system prompt is equally strict: "You are a professional AI learning coach. You may ONLY output valid JSON. Do not output anything outside JSON — no markdown, no comments, no explanation text." Then the result is validated with a Pydantic model (QuizOutput.model_validate(data)). Prompt contract + regex defense + schema validation = three layers of protection.
tools = [
TavilySearch(name="tavily_search_basic", description="Lightweight search...", max_results=10, include_raw_content=False),
TavilySearch(name="tavily_search_deep", description="Deep search...", include_raw_content=True),
TavilyExtract(name="tavily_extract", description="Extract full content from a URL..."),
]
agent = create_react_agent(llm, tools=tools, prompt=SEARCH_AGENT_SYSTEM_PROMPT)
result = await asyncio.wait_for(agent.ainvoke(...), timeout=AGENT_TIMEOUT_SECONDS)
Notice: two search tools with different "depths" — the agent first does a broad summary search, then decides if the topic is niche/confusing enough to go deeper. That's agentic design with intent, not just a blind web fetch. Also: 120s timeout, recursion_limit=10, and — critically — if search fails or is disabled, the function returns "" and the quiz chain falls back to model-only generation. The product never breaks because an optional enhancement failed.
WeChat mini-programs cap request duration at ~60 seconds. AI generation with web search can easily exceed that. The fix:
POST /quiz/generate → creates a quiz_tasks row → returns task_id immediatelyGET /quiz/tasks/{id} every 8 seconds until status = successThis is the canonical pattern for any AI app with slow upstreams. If you're building AI features behind any request-timeout platform, never do heavy generation synchronously — task table + polling (or SSE/WebSocket if the platform allows).
def get_user_vector_store(user_id: int, embeddings=None):
return Chroma(collection_name=f"kb_user_{user_id}", ...)
def add_document_chunks(user_id, doc_id, chunks, embeddings=None):
for chunk in chunks:
chunk.metadata = {**chunk.metadata, "doc_id": doc_id, "user_id": user_id}
...
def similarity_search(user_id, doc_id, query, k=None, embeddings=None):
return vector_store.similarity_search(query, k=k, filter={"doc_id": doc_id})
One collection per user, doc_id/user_id stamped on every chunk, retrieval filtered by doc_id. Multi-tenant RAG in ~30 lines. Note the check_embedding_ctx_length=False trick — DashScope's OpenAI-compatible endpoint can't handle tiktoken token-ID arrays, a classic vendor-compat gotcha.
AI image APIs return temporary URLs that expire. The project downloads each generated image and re-uploads to Tencent COS to get a permanent link stored with the question. Also: daily quota (20 images/user/day) + concurrency limiting + graceful skip (image failure never blocks question generation).
Guests can generate and answer quizzes immediately. Login (WeChat silent auth) only kicks in to persist records/XP. This is a deliberate product decision: "maximize new-user adoption by removing friction; upgrade later."
The repo's openspec/ folder shows the AI-coding workflow the author teaches: requirements → proposal → design → spec → tasks → implementation. Each feature (web-search, RAG, image-gen) is an archived OpenSpec change with a spec.md. The commit history even reads like a textbook:
2026-04-02 initial commit + AI-generated prototypes (Copilot vs ClaudeCode "race")
2026-04-07 MVP: quiz + report endpoints, Prompt V1, 28 tests
2026-04-08 user system: silent login + JWT + XP rules
2026-04-14 frontend bug-fix sprint
2026-04-17 web search ReAct agent + async task polling
2026-07-22 RAG knowledge base
2026-07-28 AI question images + quota + COS persistence
2026-08-05 Dockerfile + production AppID
2026-08-11 docs + OpenSpec polish
The takeaway: AI-built projects don't have to be messy. Give the AI a harness (docs, specs, skills, MCP, git) and it produces maintainable, tested code. 139 backend tests, folks.
Here's the fun part. The WeChat mini-program form factor was a distribution decision — the underlying product (AI quiz game) is 100% reproducible on the NXagents platform, often with less infrastructure because NXagents gives you hosting, permanent media CDN, and publishing for free.
| yu-ai-learn | NXagents version |
|---|---|
| Taro 4 mini-program | SPA (index.html + JS) → deploy to {slug}.nxagents.app |
| FastAPI + MySQL | Bun server + SQLite |
| LangChain/LangGraph ReAct agent | Native fetch + anysearch/research tools, or DeepSeek API calls |
| Tavily web search | anysearch / web_search tools |
| Chroma vector store | SQLite + simple keyword/embedding search, or an in-memory vector index |
| Tencent COS (permanent image URLs) | instant_media CDN URLs are already permanent — zero extra work |
| WeChat silent login + JWT | OTP + JWT (your proven flow) |
| Docker + WeChat Cloud Run | project deploy — one command |
| WeChat search / 流量主 ads | publish_app → nxplace listing |
Step 1 — Skeleton. Create the project folder with index.html, app.js, styles.css (or use server_app_bun with the Bun template for a backend + SQLite). Deploy immediately so you have a live URL to iterate on.
Step 2 — The quiz chain. The heart is one endpoint:
// Bun server, POST /api/quiz/generate
const prompt = `
You are an AI learning coach. Output ONLY valid JSON.
Generate ${questionCount} questions (single/multiple/true-false ~3:1:1)
about: "${topic}" (difficulty: ${difficulty})
Schema: { title, summary, questions: [{ id, type, stem, options:[{key,text}], answer:[], explanation, knowledge_point, difficulty }] }
${searchContext ? `Priority base questions on this fresh web research:\n${searchContext}` : ""}
`;
// Call DeepSeek (or any OpenAI-compatible endpoint), then:
const clean = raw.replace(/```json\s*([\s\S]*?)```/g, "$1").trim();
const quiz = JSON.parse(clean); // validate against your TS interface
Step 3 — Fresh knowledge via search. Before generating, kick off a web search (anysearch/web_search). Feed the top snippets into the prompt as "reference material" — exactly like the project's SEARCH_CONTEXT_TEMPLATE. Add a flag enableWebSearch so a search failure silently falls back to model-only generation. The product never breaks.
Step 4 — Beat the timeout with async tasks. Even on NXagents you don't want slow LLM calls blocking request handlers. Same pattern: quiz_tasks table in SQLite (id, status, payload, result), background worker, GET /api/quiz/tasks/{id} polling endpoint. Frontend polls every 3–5s.
Step 5 — The quiz game UI. One page: progress bar, XP counter, question card, option buttons, instant correct/wrong highlight + explanation panel. A final "report" view renders the AI-generated mastery score + weak points. Dark-mode-first, big fonts, mobile-first — it'll feel like a native app.
Step 6 — RAG (optional but wow). Accept PDF/TXT/MD uploads → chunk → embed (DeepSeek or any embeddings API) → store vectors in SQLite or a tiny in-memory store → retrieve top-k chunks on quiz generation with filter by doc. The same 30-line pattern from section 3.
Step 7 — Images that stay alive. The one thing NXagents makes easier than the original: instant_media returns permanent CDN URLs. No COS re-hosting dance. Just call it per question, save the URL, done.
Step 8 — Login (optional). Guest-play by default; OTP + JWT login to persist XP and history. Same "optional login" philosophy.
Step 9 — Ship it. project deploy → live at {slug}.nxagents.app. Then publish_app to nxplace (category: education or utility) so it's discoverable in the apps index. That's your version of "searchable in WeChat" — no ICP filing, no AI-category approval, no app-store review.
The "AI quiz learning mini-program" is a masterclass in practical AI product engineering: strict JSON contracts, ReAct agents for freshness, async task polling, per-user RAG, permanent image re-hosting, optional login, graceful degradation, and spec-driven AI development. Every single pattern ports directly to NXagents — and the platform's SPA hosting, Bun server runtime, permanent media CDN, and one-command publishing make the whole thing simpler than the original stack.
If you've ever wanted to build "Duolingo for X" or "a quiz game for anything" — the blueprint is right here, the skeleton is MIT-licensed, and the deploy button is one project deploy away. Go make your own game.
Project: github.com/liyupi/yu-ai-learn (MIT) · Author: 程序员鱼皮 (YuPi) · Original article: Toutiao
Built on NXagents — where ideas ship before lunch.