- "我要用 Superpowers 模式做一个电商智能客服系统,第一步先跑通纯对话。"
- 功能需求:"SSE 流式输出、逐 token 推送""用 PromptTemplate 模板化""用 with_structured_output 实现""历史消息裁剪加 token 预算控制"。
- 验收标准:"curl 调对话接口能看到流式回复""第二轮能续上第一轮的上下文""能拿到结构化 JSON"。
- 对结构化提取方式:"用A方式,你先测试一下可不可以"。
- 对
.env:"你先不要管.env里除了我告知你的信息外的其他内容。token budget目前先设置为2000"。
-
spec:
docs/superpowers/specs/2026-10-06-ch01-pure-chat-design.md。 -
分类:架构级(新项目的第一个子系统)。
-
用户确认的决策:
问题 决策 历史存储 A:服务端内存,按 session_id存储结构化提取方式 A: function_calling,提取模型关闭思考token 预算 2000,代码常量,不读取 .env实现方案 方案 1:LCEL 链,会话存储自己实现 设计 4 节(架构、接口、数据流、测试) 逐节确认,无修改 -
设计阶段做的实测(一次性探测脚本,放在 scratchpad,不进仓库):
- DeepSeek 结构化输出:
json_schema不支持;强制 tool_choice 在思考模式下返回 400;思考关闭时function_calling正常;json_object要求 prompt 含 "json"。 - LangChain 实测:
ChatOpenAI通过extra_body关闭思考后,with_structured_output(method="function_calling")对 4 条样例全部解析成功。 - FastAPI 0.142.2:
ServerSentEvent(data=...)把中文转义为\uXXXX,因此改用raw_data+json.dumps(ensure_ascii=False)。 - FastAPI 0.142.2:yield 型 SSE 端点在函数体内抛出
HTTPException时,客户端收到 200 和空流;在Depends中抛出时正常返回 422。因此预检放在依赖中。
- DeepSeek 结构化输出:
- Claude 曾计划读取
.env中的TOKEN_BUDGET,并提议调大预算。用户纠正:忽略.env中未告知的变量,预算固定为 2000。已同步修改CLAUDE.md。
- Context7 MCP 在会话启动后才添加,当前会话无法加载。用户重启会话后恢复。
- 实测结果与 mewhelp
.env注释矛盾:注释称"关掉思考,结构化输出会拿不到 tool_calls",实测是强制 tool_choice 必须关闭思考。以实测为准。 - 4 条提取样例中有 2 条质量不合格:"直接退钱,不想要了"被判为退款(可能应为退货);投诉样例的期望方案复述了问题。已在 spec 中加入判定规则,并列入样例集。
- 清理
.env:"你可以新建一个.env,只保留目前我告诉你我们需要的环境变量,其他的都删掉"。 - 审阅 spec:"spec 没问题,开始写实施计划"。
- 审阅计划:"计划没问题,开始执行 Task 1"。
- plan:
docs/superpowers/plans/2026-10-06-ch01-pure-chat.md,共 9 个任务(Task 1–6 单测驱动,Task 7 样例集验证,Task 8 人工检查,Task 9 验收演示)。 .env只保留 6 个变量;聊天、嵌入、重排均实测返回 200。- 写计划时的实测,以及据此同步到 spec 的修改:
count_tokens_approximately默认 4.0 字符/token,中文少算约一半。实测 DeepSeek:55 个中文字符约 30 token。新增常量CHARS_PER_TOKEN = 2.0。- 测试改用 FastAPI 官方的 anyio +
httpx.AsyncClient写法,不用pytest-asyncio。 - 预检顺序改为先检查预算,再获取会话锁。对外行为不变。
- 已验证测试写法:
FakeListChatModel逐字输出、error_on_chunk_number中途抛错、回调记录模型输入、RunnableGenerator模拟空 chunk、ChatOpenAI属性名、JSON 响应不转义中文。
- 与技能默认格式的差异:计划写全测试代码和 Prompt 原文;实现部分只写签名和流程要求,函数体由 Codex 完成。
- 用户要求
.env只保留已告知的 6 个变量,其余全部删除。CLAUDE.md已同步。 - 计划评审:无修改。
- 计划初稿中的
test_count_tokens_uses_chars_per_token期望值 53 是推算的,实测为 55。已改为与count_tokens_approximately(..., chars_per_token=2.0)比较,不写死数值。
- "计划没问题,开始执行 Task 1"。
- Codex 创建
pyproject.toml、.python-version(3.12)、uv.lock、app/config.py、tests/conftest.py、tests/test_config.py。Codex 先确认测试失败,再实现。 - 依赖:fastapi ≥ 0.142、langchain-core ≥ 1.6、langchain-openai ≥ 1.6、pydantic-settings ≥ 2.15、uvicorn ≥ 0.54;开发依赖 pytest ≥ 9、httpx ≥ 0.28.1。
- Claude 审查:代码与计划一致。
uv run pytest -q结果 4 passed。用真实.env加载get_settings(),读到deepseek-v4-flash、adaptive,密钥非空。 - Claude 在
.gitignore中加入.venv/、__pycache__/、.pytest_cache/。
- 无。
- Codex 默认沙箱禁止网络,也不能写 uv 缓存目录,会导致
uv add失败。调用时加了-c sandbox_workspace_write.network_access=true和writable_roots(~/.cache/uv、~/.local/share/uv)。一次通过,无返工。
- "继续执行 Task 2"。
- Codex 创建
app/llm.py(build_chat_model、build_extract_model、get_chat_model)和tests/test_llm.py。Codex 先确认测试失败,再实现。 - Claude 审查:
- 实现与计划一致;
get_extractor按计划留到 Task 6。 tests/test_llm.py与计划中的测试代码逐字一致(用 diff 核对)。uv run pytest -q:8 passed。
- 实现与计划一致;
- Claude 用真实上游做冒烟检查:聊天模型返回 23 个思考 token(
adaptive生效);提取模型没有思考 token(disabled生效)。
- 无。
- 无。
- "继续执行 Task 3"。
- Codex 创建
app/prompts.py、app/schemas.py、tests/test_prompts.py、tests/test_schemas.py。Codex 先确认测试失败,再实现。 - Claude 审查:
- 用脚本把 4 个文件与计划中的代码块逐一比较,全部一致。Prompt 原文和 Field description 没有被改写。
uv run pytest -q:17 passed。- 用
convert_to_openai_tool(AfterSalesRequest)检查发给上游的函数 schema:6 个枚举值、3 个字段的 description 都在其中;order_id可为 null,不在 required 中。
- 无。
- 无。
- "继续执行 Task 4"。
- Codex 创建
app/session.py、app/context.py、tests/test_session.py、tests/test_context.py。Codex 先确认测试失败,再实现。 - Claude 审查:
SessionStore.get_or_create和build_history的实现与计划一致。- 两个测试文件与计划逐字一致(脚本比对)。
uv run pytest -q:25 passed。
- Claude 计算了真实预算余量(
CHARS_PER_TOKEN = 2.0):- System Prompt:296 字,154 token。
- 最长消息(2000 字):1005 token。
- 最坏情况固定部分:1159 token,低于 2000。结论:在
MAX_INPUT_CHARS = 2000下,budget_exceeded在生产中不会触发,只是保护性检查;历史至少有 841 token 余量,短消息时约 1840 token。
- 无。
- 无。
- "继续执行 Task 5"。
- Codex 创建
app/services/chat.py、app/api/chat.py、app/api/health.py、app/main.py,扩展tests/conftest.py,创建tests/test_chat_api.py、tests/test_chat_service.py。3 个测试文件与计划逐字一致(脚本比对)。 - 第一版
uv run pytest -q:38 passed。 - Claude 用真实服务(uvicorn + DeepSeek)验证:
- 验收 1:逐 token 推送,98 个
token事件,最后是done。 - 验收 2:第二轮回答"您提到的是蓝牙耳机,问题是左耳没有声音",上下文接上。
- 流式期间同一会话的第二个请求返回 409
session_busy。 - 客户端中途断开后,同一会话可以继续请求(锁已释放),且断开那一轮没有写入历史。
- 验收 1:逐 token 推送,98 个
- 修复后
uv run pytest -q:39 passed。计划 Task 5 已补记修订说明。
- 无。
- 锁泄漏缺陷(计划设计缺陷,Claude 审查发现): 计划让依赖获取锁、端点生成器释放锁。如果预检之后、生成器开始之前出错,锁永远不释放,该会话之后一律返回 409。已复现:把
get_chat_model改为抛异常,第 1 次 500,第 2 次 409。- 修复:用 Context7 查到 FastAPI ≥ 0.118 的 yield 依赖(默认
scope="request")在流式响应发送完之后才执行退出代码。把prepare_chat_turn改为 yield 依赖,在finally中释放锁。Codex 先加回归测试并确认失败,再修复。 - 同时把 Codex 照抄的占位注释("按流程要求实现")换成说明性注释。
- 修复:用 Context7 查到 FastAPI ≥ 0.118 的 yield 依赖(默认
- 测试操作失误: Claude 第一次真实验证时用
head -c截断了第 1 轮输出,等于客户端断开,所以第 2 轮没有上下文。这一现象恰好符合"断开不写历史"的设计。换新会话完整跑两轮后通过。 - 留给 Task 8 的 Prompt 问题: 用户问"我上一句问了什么?"时,模型判为无关问题并拒绝,说明行为约束第 4 条过严;回复中出现 Markdown 粗体
**,curl 和纯文本界面会显示星号。
- "继续执行 Task 6"。
-
Codex 创建
app/services/extract.py、app/api/extract.py、tests/test_extract_api.py,在app/llm.py中添加get_extractor,在app/main.py中注册路由。测试文件与计划逐字一致(脚本比对)。Codex 先确认测试失败,再实现。 -
Claude 审查:实现与计划一致;对外错误只返回固定文案,原始输出和解析错误只写服务端日志;没有循环导入。
uv run pytest -q:44 passed。 -
Claude 用真实服务验证验收 3(
curl /extract):输入 输出 订单号 A12345 的耳机左耳没声音,我想换个新的。 A12345/ 换货 / 更换新的耳机羊毛衫缩水……直接退钱吧,不想要了。 20261003-889/ 退货 / 退款快递太慢了……我要投诉! null/ 投诉 / 未说明请问七天无理由退货怎么算? null/ 咨询 / 未说明只含空格 422 响应中的中文直接显示,没有转义。设计阶段的 2 条问题样例(羊毛衫判为退款、投诉复述问题)在新 Prompt 下都已正确。
- 无。
- 无。计划写的全量测试数是 43,因 Task 5 多加了 1 个回归测试,实际为 44。已在任务描述中提前告知 Codex。
- "标注已审核,继续执行 Task 7"。
-
Codex 创建
evals/extract_samples.jsonl(与计划逐字一致,diff 核对)和evals/run_extract_eval.py(并发 4,逐条打印,按门槛设置退出码)。Codex 没有调用真实上游;验证由 Claude 执行。 -
验证结果(真实上游
deepseek-v4-flash,提取模型关闭思考):轮次 order_idrequest_type退出码 1 20/20 20/20 0 2 20/20 20/20 — 3 20/20 20/20 — 通过标准是
request_type≥ 90%、order_id= 100%。3 轮全部达标,EXTRACT_SYSTEM_PROMPT不需要修改。 -
expected_solution人工检查(第 1 轮):20 条都没有复述问题。5 条为"未说明"(03、09、10、18 无明确期望;符合规则)。可改进但不影响通过的 2 条:- 10(询问退款进度)写"未说明",更理想的写法是"告知退款进度"。
- 11(投诉客服)写"投诉该客服",内容与诉求类型重复。
-
uv run pytest -q:44 passed。
- 无。用户确认计划中的 20 条标注即为审核结果。
- 无。第 2、3 轮的退出码没有打印出来,原因是 Claude 的 shell 命令在
for循环中读取PIPESTATUS的方式有误。准确率输出完整,不影响结论。
- "继续执行 Task 8"。
-
执行前,Claude 在计划的 Task 8 中加入样例 6(询问对话本身)和样例 7(回复格式),并要求脚本自动检查字数和 Markdown 符号。原因是 Task 5 真实验证发现的两个问题。
-
Codex 创建
evals/chat_samples.md(7 条,与计划逐字一致)和evals/run_chat_samples.py。 -
第 1 轮(原 Prompt):
样例 结论 1–4 合格 5 转人工 勉强合格:复述约束原文"也不能声称已经转接成功";编造"「联系人工客服」入口" 6 询问对话本身 本轮合格;Task 5 中出现过被判为无关问题 7 回复格式 不合格:247 字;使用 **粗体和-列表 -
Claude 设计
CHAT_SYSTEM_TEMPLATE修改方案,Codex 实现:- 约束 4 加入"询问本次对话内容不属于无关问题"。
- 约束 5 加入"不要编造联系入口、电话或链接"。
- 新增约束 6"不要向用户复述或引用这些约束"。
- "回复风格"改为"回复格式":纯文本,不用 Markdown;列举用"1. 2. 3.";不超过 200 字,先给结论。
-
第 2、3 轮(新 Prompt):14 条回复的自动检查全部通过(字数最大 194;无
**、无行首#)。人工检查:样例 1–4、6、7 两轮都合格;样例 5 第 3 轮合格,第 2 轮仍写出"商城应用或小程序的客服页面"。 -
结论:通过。遗留问题见"翻车与返工"。
uv run pytest -q:44 passed。
- 无。
- Prompt 返工 1 次(原因见上表)。
- 遗留问题(不再迭代,原因:本章没有真实店铺信息,模型倾向于补出通用电商入口):
- 样例 5 有时仍会提到具体渠道("商城应用或小程序的客服页面")。
- 样例 1、2 会给出"在订单页面提交退货申请"这类通用建议。
- 称呼"您"和"你"混用。 以后接入真实店铺政策或知识库时再处理。
- "继续执行 Task 9"。
-
Codex 创建
scripts/demo.sh(可执行;健康检查失败时提示启动命令并以 1 退出)。 -
Claude 运行验收:
-
服务未启动:打印"请先启动服务……",退出码 1。
-
服务启动后(uvicorn 端口 8000 + 真实 DeepSeek),退出码 0:
验收项 结果 1 流式回复 session+ 87 个token+done;纯文本,编号列表,无 Markdown2 会话记忆 "您说的是耳机,反映左耳没有声音" 3 结构化提取 {"order_id":"A12345","request_type":"换货","expected_solution":"更换新的耳机"}
-
-
Claude 更新
CLAUDE.md:项目状态、常用命令(含单个测试命令,已实测)、模块分层,以及 7 条必须保持的设计约束及其依据。
- 无。
- 无。
- "开始code REVIEW"。
-
评审方式:按 superpowers:requesting-code-review,派一个全新的 reviewer 子代理(只读,不调用真实上游,不读
.env)。范围8dc3069..22cde0d。输入:spec、plan、CLAUDE.md、dev-notes,以及 3 处有意偏离。 -
reviewer 结论:With fixes。0 个 Critical,1 个 Important,5 个 Minor。reviewer 认为 3 处偏离都合理,并离线实测确认锁在流式期间被持有。
-
Claude 逐条核实,全部成立,全部处理:
# 级别 问题 核实 处理 1 Important 锁覆盖整个流没有自动化测试; test_chat_service的测试名和注释在偏离 1 后已过时读代码确认 Codex 加 test_lock_held_during_stream_and_released_after;服务层测试删去锁代码并改名2 Minor spec 未同步偏离 1、2 读 spec 确认 4 处 Claude 改 spec 9.2、9.4、6.1、10.3 3 Minor 上游返回空回复时写入空 AIMessage并发送done读代码确认 Codex 改为发送 error、不写历史,加测试4 Minor CHAT_THINKING=空字符串会发送{"type": ""}已复现 Codex 加 env_ignore_empty=True,加测试5 Minor 未设置 CHAT_THINKING时/extract可能失效真实上游证实:DeepSeek 默认开启思考,不发 thinking字段时强制 tool_choice 返回 400在 llm.py注释、spec 第 5 节、CLAUDE.md中写明"使用 DeepSeek 必须设置CHAT_THINKING"6 Minor 系统提示变量在两处重复 读代码确认 Codex 新增 chat_prompt_vars(),计数和发送共用,加测试 -
修复后验证:
- Codex 先加 4 个新测试:3 个失败,锁回归测试通过(它是回归测试)。修复后 48 passed。
- Claude 做变异检查:在临时副本中把锁依赖改为
scope="function",test_lock_held_during_stream_and_released_after失败。说明新测试能抓到这类回退。 - Claude 用真实服务重跑
scripts/demo.sh:退出码 0,114 个token事件,两轮都以done结束,第二轮回答"您刚才说的是耳机,上周购买,左耳没有声音",提取结果正确。
-
reviewer "Declined to judge" 的 7 项,Claude 同意不在本章处理:会话 ID 可被猜测续接(需鉴权,后续章节);会话无上限无过期(spec 已知限制);日志含用户原文(spec 要求记录原始输出);
finish_reason固定为stop(spec 规定);样例 5 渠道描述和称呼混用(阶段 10 遗留);无 lint 工具;未核对 evals 和 demo 的真实运行(dev-notes 已记录)。
- 无。
- 偏离 1 修复时只加了"依赖出错时释放锁"的回归测试,漏了"流式期间持锁"的测试,并保留了过时的测试名和注释。reviewer 发现,本阶段补齐。
- 偏离 1、2 只改了 plan,没有同步 spec,与偏离 3 的处理不一致。本阶段补齐。
- 设计阶段没有覆盖"未设置
CHAT_THINKING时 DeepSeek 默认开启思考"这一情况,spec 第 5 节的矩阵只考虑了 GPT、Ollama 的兼容性。
- "开始 finish"。
- 按 superpowers:finishing-a-development-branch 执行:
- 全量测试:
uv run pytest -q,48 passed。 - 环境检测:普通仓库(非 worktree),当前分支
main,与origin/main同步,没有功能分支。 - 分支收尾:所有提交已在
main上并已推送,技能菜单中的"本地合并"和"创建 PR"不适用,按"保持现状"处理。
- 全量测试:
- 交付内容:
- 演示命令:先执行
uv run uvicorn app.main:app --port 8000,再执行bash scripts/demo.sh。 - 测试结果:单测 48 passed;提取样例集 3 轮均为 20/20;客服样例 7 条 × 2 轮自动检查全部通过;真实服务验收 3 项全部通过(阶段 11、12)。
- 产出文件:spec
docs/superpowers/specs/2026-10-06-ch01-pure-chat-design.md;plandocs/superpowers/plans/2026-10-06-ch01-pure-chat.md;本文件dev-notes/ch01.md。
- 演示命令:先执行
- 无。
- 无。
- 流程备注:ch01 全程直接在
main上提交。后续章节可以先建功能分支,finish 时走 PR,使分支收尾步骤真正生效。
按项目规则,聊天页面不走 brainstorm、TDD、code review 流程。Claude 把用户描述转成任务交给 Codex,再用浏览器实测。
- "一个客服对话 Web 界面,消息气泡排布,对接 SSE 接口把回复逐字渲染出来,能连续多轮聊"。
- "浏览器打开聊天页,发一个问题能看到回复逐字蹦出来,接着追问一句上下文也续得上"。
- Codex 新建
app/web/index.html(单文件,无外部依赖)和app/api/web.py(GET /返回页面,FileResponse用法已用 Context7 核对),并在app/main.py注册路由。 - 页面要点:
fetch+ReadableStream解析 SSE(EventSource不支持 POST);保存session事件中的session_id实现多轮;"新对话"按钮;三点"正在输入"动画;输入法组字时回车不发送;错误显示在气泡内。 - 验证:用 Playwright 驱动本机 Chrome(无头)打开
http://127.0.0.1:8000/,发两轮消息并截图(桌面 900px、手机 390px)。第二轮回答"您刚才说的是上周购买的蓝牙耳机,左耳没有声音"。uv run pytest -q:48 passed。
- 无。
- claude-in-chrome 技能在本会话不可用,改用 Playwright + 本机 Chrome。
- 逐字效果返工 1 次:DeepSeek 约 0.5 秒推完整段回复,页面收到
done时把队列剩余字符一次性插入,180 字在 0.5 秒内显示完。改为收到done后继续按 20ms 节奏输出,每次 max(2, ceil(剩余/60)) 个字符,队列清空才恢复输入。修复后 130 字约 1.3 秒逐步显示。