Skip to content

Latest commit

 

History

History
407 lines (268 loc) · 22.4 KB

ch01.md

File metadata and controls

407 lines (268 loc) · 22.4 KB

ch01 纯对话:开发记录

阶段 1:brainstorm 定稿(2026-10-06)

用户的关键原话

  • "我要用 Superpowers 模式做一个电商智能客服系统,第一步先跑通纯对话。"
  • 功能需求:"SSE 流式输出、逐 token 推送""用 PromptTemplate 模板化""用 with_structured_output 实现""历史消息裁剪加 token 预算控制"。
  • 验收标准:"curl 调对话接口能看到流式回复""第二轮能续上第一轮的上下文""能拿到结构化 JSON"。
  • 对结构化提取方式:"用A方式,你先测试一下可不可以"。
  • 对 .env:"你先不要管.env里除了我告知你的信息外的其他内容。token budget目前先设置为2000"。

Claude 的关键产出

  • 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,不进仓库):

    1. DeepSeek 结构化输出:json_schema 不支持;强制 tool_choice 在思考模式下返回 400;思考关闭时 function_calling 正常;json_object 要求 prompt 含 "json"。
    2. LangChain 实测:ChatOpenAI 通过 extra_body 关闭思考后,with_structured_output(method="function_calling") 对 4 条样例全部解析成功。
    3. FastAPI 0.142.2:ServerSentEvent(data=...) 把中文转义为 \uXXXX,因此改用 raw_data + json.dumps(ensure_ascii=False)。
    4. FastAPI 0.142.2:yield 型 SSE 端点在函数体内抛出 HTTPException 时,客户端收到 200 和空流;在 Depends 中抛出时正常返回 422。因此预检放在依赖中。

用户拒绝或纠偏的内容

  • Claude 曾计划读取 .env 中的 TOKEN_BUDGET,并提议调大预算。用户纠正:忽略 .env 中未告知的变量,预算固定为 2000。已同步修改 CLAUDE.md。

翻车与返工

  • Context7 MCP 在会话启动后才添加,当前会话无法加载。用户重启会话后恢复。
  • 实测结果与 mewhelp .env 注释矛盾:注释称"关掉思考,结构化输出会拿不到 tool_calls",实测是强制 tool_choice 必须关闭思考。以实测为准。
  • 4 条提取样例中有 2 条质量不合格:"直接退钱,不想要了"被判为退款(可能应为退货);投诉样例的期望方案复述了问题。已在 spec 中加入判定规则,并列入样例集。

阶段 2:计划评审通过(2026-10-06)

用户的关键原话

  • 清理 .env:"你可以新建一个.env,只保留目前我告诉你我们需要的环境变量,其他的都删掉"。
  • 审阅 spec:"spec 没问题,开始写实施计划"。
  • 审阅计划:"计划没问题,开始执行 Task 1"。

Claude 的关键产出

  • 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 的修改:
    1. count_tokens_approximately 默认 4.0 字符/token,中文少算约一半。实测 DeepSeek:55 个中文字符约 30 token。新增常量 CHARS_PER_TOKEN = 2.0。
    2. 测试改用 FastAPI 官方的 anyio + httpx.AsyncClient 写法,不用 pytest-asyncio。
    3. 预检顺序改为先检查预算,再获取会话锁。对外行为不变。
    4. 已验证测试写法: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) 比较,不写死数值。

阶段 3:Task 1 完成——项目骨架与配置(2026-10-06)

用户的关键原话

  • "计划没问题,开始执行 Task 1"。

Claude 的关键产出

  • 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)。一次通过,无返工。

阶段 4:Task 2 完成——模型创建与思考参数(2026-10-06)

用户的关键原话

  • "继续执行 Task 2"。

Claude 的关键产出

  • 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 生效)。

用户拒绝或纠偏的内容

  • 无。

翻车与返工

  • 无。

阶段 5:Task 3 完成——Prompt 模板与数据模型(2026-10-06)

用户的关键原话

  • "继续执行 Task 3"。

Claude 的关键产出

  • 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 中。

用户拒绝或纠偏的内容

  • 无。

翻车与返工

  • 无。

阶段 6:Task 4 完成——会话存储与上下文裁剪(2026-10-06)

用户的关键原话

  • "继续执行 Task 4"。

Claude 的关键产出

  • 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。

用户拒绝或纠偏的内容

  • 无。

翻车与返工

  • 无。

阶段 7:Task 5 完成——流式对话接口(2026-10-06)

用户的关键原话

  • "继续执行 Task 5"。

Claude 的关键产出

  • 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。
    • 客户端中途断开后,同一会话可以继续请求(锁已释放),且断开那一轮没有写入历史。
  • 修复后 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 照抄的占位注释("按流程要求实现")换成说明性注释。
  • 测试操作失误: Claude 第一次真实验证时用 head -c 截断了第 1 轮输出,等于客户端断开,所以第 2 轮没有上下文。这一现象恰好符合"断开不写历史"的设计。换新会话完整跑两轮后通过。
  • 留给 Task 8 的 Prompt 问题: 用户问"我上一句问了什么?"时,模型判为无关问题并拒绝,说明行为约束第 4 条过严;回复中出现 Markdown 粗体 **,curl 和纯文本界面会显示星号。

阶段 8:Task 6 完成——结构化提取接口(2026-10-06)

用户的关键原话

  • "继续执行 Task 6"。

Claude 的关键产出

  • 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。

阶段 9:Task 7 完成——提取 Prompt 样例集验证(2026-10-06)

用户的关键原话

  • "标注已审核,继续执行 Task 7"。

Claude 的关键产出

  • Codex 创建 evals/extract_samples.jsonl(与计划逐字一致,diff 核对)和 evals/run_extract_eval.py(并发 4,逐条打印,按门槛设置退出码)。Codex 没有调用真实上游;验证由 Claude 执行。

  • 验证结果(真实上游 deepseek-v4-flash,提取模型关闭思考):

    轮次 order_id request_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 的方式有误。准确率输出完整,不影响结论。

阶段 10:Task 8 完成——客服 System Prompt 人工检查(2026-10-06)

用户的关键原话

  • "继续执行 Task 8"。

Claude 的关键产出

  • 执行前,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 实现:

    1. 约束 4 加入"询问本次对话内容不属于无关问题"。
    2. 约束 5 加入"不要编造联系入口、电话或链接"。
    3. 新增约束 6"不要向用户复述或引用这些约束"。
    4. "回复风格"改为"回复格式":纯文本,不用 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 次(原因见上表)。
  • 遗留问题(不再迭代,原因:本章没有真实店铺信息,模型倾向于补出通用电商入口):
    1. 样例 5 有时仍会提到具体渠道("商城应用或小程序的客服页面")。
    2. 样例 1、2 会给出"在订单页面提交退货申请"这类通用建议。
    3. 称呼"您"和"你"混用。 以后接入真实店铺政策或知识库时再处理。

阶段 11:Task 9 完成——验收演示与文档(2026-10-06)

用户的关键原话

  • "继续执行 Task 9"。

Claude 的关键产出

  • Codex 创建 scripts/demo.sh(可执行;健康检查失败时提示启动命令并以 1 退出)。

  • Claude 运行验收:

    • 服务未启动:打印"请先启动服务……",退出码 1。

    • 服务启动后(uvicorn 端口 8000 + 真实 DeepSeek),退出码 0:

      验收项 结果
      1 流式回复 session + 87 个 token + done;纯文本,编号列表,无 Markdown
      2 会话记忆 "您说的是耳机,反映左耳没有声音"
      3 结构化提取 {"order_id":"A12345","request_type":"换货","expected_solution":"更换新的耳机"}
  • Claude 更新 CLAUDE.md:项目状态、常用命令(含单个测试命令,已实测)、模块分层,以及 7 条必须保持的设计约束及其依据。

用户拒绝或纠偏的内容

  • 无。

翻车与返工

  • 无。

阶段 12:code review 结论(2026-10-06)

用户的关键原话

  • "开始code REVIEW"。

Claude 的关键产出

  • 评审方式:按 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 的兼容性。

阶段 13:finish(2026-10-06)

用户的关键原话

  • "开始 finish"。

Claude 的关键产出

  • 按 superpowers:finishing-a-development-branch 执行:
    1. 全量测试:uv run pytest -q,48 passed。
    2. 环境检测:普通仓库(非 worktree),当前分支 main,与 origin/main 同步,没有功能分支。
    3. 分支收尾:所有提交已在 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;plan docs/superpowers/plans/2026-10-06-ch01-pure-chat.md;本文件 dev-notes/ch01.md。

用户拒绝或纠偏的内容

  • 无。

翻车与返工

  • 无。
  • 流程备注:ch01 全程直接在 main 上提交。后续章节可以先建功能分支,finish 时走 PR,使分支收尾步骤真正生效。

附:聊天页面(Vibe Coding,2026-10-06)

按项目规则,聊天页面不走 brainstorm、TDD、code review 流程。Claude 把用户描述转成任务交给 Codex,再用浏览器实测。

用户的关键原话

  • "一个客服对话 Web 界面,消息气泡排布,对接 SSE 接口把回复逐字渲染出来,能连续多轮聊"。
  • "浏览器打开聊天页,发一个问题能看到回复逐字蹦出来,接着追问一句上下文也续得上"。

Claude 的关键产出

  • 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 秒逐步显示。