20 · 实战三:逐请求追踪、双语言调用与反例验收¶
前两章已经跑过0–5检查点。本章教你检查实际数据与失败分支,判断改动是否有效。实验固定rc.2 CLI/SDK;无密钥路径使用本地Messages SSE服务,真实loop、工具、日志和存储都运行。
20.1 从干净目录重建¶
unzip deepseek-agent-labs.zip
cd examples
npm ci --ignore-scripts
npm run check
npm run build
node dist/checkpoints.js 0
node dist/checkpoints.js 1
node dist/checkpoints.js 2
node dist/checkpoints.js 3
node dist/checkpoints.js 4
node dist/checkpoints.js 5
实际验证使用Node24.14.0/npm11.9.0;build生成dist,测试不会自动替你编译。每阶段支持第三个位置参数指定产物目录,例如 node dist/checkpoints.js 1 ./my-trace。默认artifacts目录是读者本地数据,不包含在实验发行包中。
Python另行准备,Node先跑通后再执行:
python3 -m venv .venv
.venv/bin/python -m pip install -r python/requirements.txt
export DSH_COURSE_PYTHON="$PWD/.venv/bin/python"
npm test
npm run demo
npm run demo:python
Debian/Ubuntu若 python3 -m venv 报ensurepip unavailable,可安装与解释器匹配的python3-venv包;已有uv时,也可改用 uv venv .venv,再 uv pip install --python .venv/bin/python -r python/requirements.txt。这是环境准备问题,不应改用未配对的PyPI runtime绕过。
Windows解释器通常是 .venv/Scripts/python.exe;本次没有Windows/macOS实测。npm ci固定依赖解析,不等于验证了所有平台或依赖行为。
20.2 每次请求究竟增加了什么¶
用任何JSON查看器打开阶段1的requests.json。也可在examples复制:
node --input-type=module - <<'JS'
import { readFile } from 'node:fs/promises'
const requests = JSON.parse(await readFile('artifacts/checkpoint-1/requests.json', 'utf8'))
for (const [index, request] of requests.entries()) {
console.log('REQUEST', index + 1)
console.log(JSON.stringify(request.body.messages, null, 2))
}
JS
三次请求的关键截面如下,省略不变的system、tools、模型配置和协议扩展;完整数据保存在文件。
| 请求 | 已有消息 | 本次fixture响应 | 新增证据 |
|---|---|---|---|
| 1 | 用户问题 | search_notes,call-1,query=Session | 尚无资料 |
| 2 | 问题+call-1+call-1结果 | read_note,call-2,id=session | 只有ID和24字预览 |
| 3 | 前述消息+call-2+call-2结果 | 答案与[session] | 完整事实首次存在 |
第二请求的结果块:
{"type":"tool_result","tool_use_id":"call-1","content":[{"type":"text","text":"{\"matches\":[{\"id\":\"session\",\"preview\":\"Session 保存请求与工具的事件日志。搜索摘\"}]}"}],"is_error":false}
第三请求新增结果块:
{"type":"tool_result","tool_use_id":"call-2","content":[{"type":"text","text":"[session] Session 保存请求与工具的事件日志。搜索摘要只用于选择资料。完整证据:失败 attempt 只留诊断,不加入下一次模型历史。"}],"is_error":false}
这两个user-role结果块是Messages协议表达。DSH的逻辑日志使用tool-role message与toolCallId,见06章。切勿把provider序列化形态反推成Session的内部数据类型。
20.3 对着请求找日志,不靠固定seq猜测¶
node --input-type=module - <<'JS'
import { readFile } from 'node:fs/promises'
const events = JSON.parse(await readFile('artifacts/checkpoint-1/events.json', 'utf8'))
for (const event of events) {
const data = event.data
console.log(event.seq, event.type,
data.callId ?? data.message?.toolCallId ?? data.reason?.kind ?? '')
}
JS
找到 assistant/message里的tool-call,接着找到同ID的tool/call和tool/result。前者是模型提出的动作,后两者是执行意图与结果。结果在下一step构造request前已经提交;如果它没有进入下一请求,要查surface/adapter,而不是先怀疑SDK。
本次三请求通常只有一条request/header,因为provider/model/schema没变。不能以header数当HTTP数。HTTP日志是请求发送证据,step/start是准入步骤证据,assistant/attempt是模型失败结算证据,各自回答不同问题。
完整HTTP body还包含DeepSeek日志扩展及包清单;查聊天历史时看system/messages,查网络发送边界时看完整body。产物只用本地固定资料,不包含真实API key;把自己的资料加入后,应按实际内容管理这些文件。
20.4 三个容易骗过happy path的反例¶
注册但拒绝。 阶段3仍公布read_note,fixture仍提出调用;pre gate拒绝,provider读取计数测试为0,tool_result.is_error=true,下一请求无全文。turn completed,但taskStatus为insufficient-evidence。说明“正常结束”不是“回答了问题”。
读到证据但没预算回答。 阶段4的read_note成功,evidenceRead为true;第三HTTP未发,最终文本为空,turn blocked。检查点要求budget-exhausted,不会把已经取到资料当交付完成。
假答案缺少前置证据。 checkpoints.test.mjs向fixture发一个错误tool_use_id,哪怕content写着evidence,服务仍返回400而不发送成功答案。fixture校验的是指定结果块,不是仅在完整请求任意位置搜到某个词。这让“删掉read步骤却还得到预写正确答案”成为失败。
此外原有测试保留真实bash越权尝试(标记文件未出现)、HTTP401(assistant/attempt和error结局)、同runtime同Session第二轮(请求含上一回答)、无效参数、取消前不dispatch、post block不回滚与异步gate等待。
20.5 Python是同一运行时的另一宿主¶
npm run demo:python使用同一JSON provider和两个工具。Node启动HTTP fixture并准备patch,然后用execFile启动 python/notes-agent.py;Python通过公开dsh_bin/profile/patches/dsh_home参数启动同版npm CLI。
rc.2官方Python SDK五个文件原样保留于 python/source_vendor,SOURCE.json记录完整commit/字节哈希与MIT许可。未把PyPI的旧runtime混入;没有私有argv或fake SDK。Python env与父环境合并,所以Node先给Python一个环境白名单。
预期结果是3次请求、2个工具结果、finishReason completed、Python SDK: source session.。这一路fixture也检查call-1摘要与call-2全文。Python提供finish_reason便利字段,TS通过turn/end读取;两种语言不能照搬属性名。
20.6 正常关闭与冷读的证据范围¶
阶段5在close之后调用官方JSONL provider,只读打开同Session。cold-read.json保存header和完整逻辑events,并逐seq核对SDK事件。它回答“正常退出后,这段事实还在吗”。它没有执行kill-9,没有模拟断电,没有从SDK冷resume,也没有验证所有旧格式迁移。
因为正常SDK run结束的idle不是flush回执,检查点先关闭runtime。生产应用若需要运行中承诺持久提交,应使用明确的flush屏障并处理失败;不能为了出一个文件就任意sleep一秒,然后宣称耐久性已经保证。
20.7 测试是证据索引,不是质量分数¶
| 测试组 | 关键行为 | 需要读的产物/断言 |
|---|---|---|
| contracts | 生命周期、schema、gate、取消、输出value/render | provider读计数、工具schema、canonical value、错误结果 |
| sdk-runtime | 实际CLI/协议/loop与历史 | 真实请求、标记文件、turn原因、同Session内容 |
| checkpoints | 六阶段差异与冷读 | requests/events/summary/cold-read;错callId反例 |
| python-runtime | 官方源码Python SDK到同版CLI | 请求数、工具结果、finish reason |
测试原输出与本轮作者检查留在research/astra;独立审查由25章记录。旧下载重建报告只证明旧版artifact,不自动覆盖本轮新文件。未测分支见24章;测试多一项只有在它能排除一个真实误判时才有价值。
20.8 用真实模型检验另一类问题¶
这条命令会调用你配置的实际provider,本轮没有执行。默认endpoint为 https://api.deepseek.com/anthropic。进入真实评估前,先准备“已知答案、无资料、冲突资料、错误ID、受限资料、预算不足”六类任务。记录工具选择、全文读取、引用是否支持结论、错误完成率与真实usage。协议fixture证明运行机制,真实评估才回答模型是否会在不被脚本安排的情况下完成任务。