lab04 工程化(对应教程 Part 5)

把玩具拧成生产级工具:状态能恢复、输出能丝滑、运行能观测、危险动作有人拦。


墨叔 vs 老哥:四个工程维度的实战对话

老哥:墨叔,前面四个 Part 我把 agent 跑通了——能想、能调工具、能记上下文。这还不够吗?

墨叔:不够。那叫”实验室玩具”。生产级 agent 要过四道坎,哥你后端那点功力正好用上。咱一个一个来。

① 流式输出:为什么 Claude Code 丝滑

老哥:丝滑不就是体验好点嘛,至于单开一节?

墨叔:你跑过 npm install 吧?它一行行刷进度你就安心,卡在 fetchMetadata 不动你就想 kill。模型生成回复本来就是一个 token 一个 token 蹦的——它不会”想好整段再吐”。你代码把 token 全攒着最后甩出来,中间屏幕死寂,用户以为挂了。main.py 的 stream_print 就是把最终答案逐字吐出,模拟 SSE/chunked 那套增量渲染。

老哥:那流式能让我任务跑更快?

墨叔:不能,真实完成时间不变。它只是把”第一字节到达”从十秒压到零点几秒。但它解锁了可打断——你三秒看到”它要删生产库”,立刻 Ctrl-C,后面本要烧的 token 根本没产生。所以丝滑不是玄学体验,是成本控制的工程手段。光流式不够,得配”增量渲染 + 随时可打断 + 中间状态可见”,才叫可控。

② 状态持久化 / checkpoint:agent 不是一次性脚本

老哥:我让 agent 跑”重构支付模块”这种大活,转了 27 圈终端被我关了,难道从头再来?

墨叔:生产级答案必须是”从最后干净状态接着跑”。核心三件套:会话持久化(纸条落盘,即事件溯源)+ checkpoint(一致性快照)+ 断点续跑(启动先读回纸条)。main.py 里 save_messages/load_messages 把 messages 序列化成 json,崩了 load 回来,run_agent(messages=recovered) 从最后一条消息续跑。

老哥:这不就是我后端的 WAL + 消费位点恢复吗?

墨叔:一字不差。Claude Code 的 resume 就是这么干的——纸条以 JSONL 存本地,下次读回来你以为它”记得你”,其实只是把文件读回来了。铁律:状态用事件溯源重放,副作用用快照对齐,二者别混。把”改文件”也当事件重放,会把已改对的又改一遍。

③ 可观测性:没有观测就没有迭代

老哥:加几行 print 不就完了?

墨叔:agent 是概率程序,同样目标今天走这条路径明天走那条,你根本复现不了那个错。所以你不能靠现场调试,只能靠轨迹回放。三层:结构化日志记事实、trace 用唯一 trace_id 串起一棵动作树、eval 把观测变仪表盘。main.py 的 TraceCollector 每步记”步骤/工具/耗时/token”,最后打印一条 trace 报告——和你用 Jaeger 看微服务调用链同构,只是”服务”换成了”模型决策和工具”。

老哥:记太全本身不也烧钱?

墨叔:对,所以生产上采样 + 分级:普通任务抽样,但凡触发 HITL、失败、成本超阈值的,全量留痕。这些才是你最该回放的”案发现场”。

④ 安全护栏 HITL:危险动作谁拍板

老哥:我代码里没给它删库工具,它删不了。

墨叔:只要暴露了”执行 shell”或”发邮件”,护栏只靠模型自觉,等于把保险箱钥匙挂小偷够得着的地方。三道纵深防御:最小权限(它能不能)+ 沙箱(搞砸了伤多深)+ HITL(致命动作谁拍板)。main.py 的 require_confirm 把 deploy 包起来——模型只有”申请权”,执行前过人类这关。demo 里用环境变量模拟”人拒绝 / 人确认”两个分支。

老哥:那我每一步都问人,最安全?

墨叔:错。每一步问,你点 50 次 yes 后必然无脑点,HITL 就形同虚设(警报疲劳)。按”错误代价”分级:读/查/跑测试全自动,删/发对外/部署才问人。护栏要”用户感觉不到、但真危险时一定拦得住”,厚到碍事用户就会绕开它。


运行方式

# 默认 mock:零依赖、零 API key,直接跑
python main.py
 
# 想接真实模型(需 OPENAI_API_KEY)
export OPENAI_API_KEY=sk-xxx
AGENT_MODE=real python main.py

依赖:仅 Python 标准库(json / time / os / sys),无需 pip install。

四个 demo 依次演示:

demo工程维度看什么
demo_streaming流式输出最终答案逐字吐出
demo_checkpoint状态持久化跑 1 步”崩溃”→ load → 续跑
demo_observability可观测性trace 报告(步骤/工具/耗时/token)
demo_hitl安全护栏拒绝/确认两分支

【思考题】

  1. 断点续跑时,如果崩溃点恰好在”模型已决定调工具、但工具还没执行”的中间态,你恢复后应该重放这个未执行的决策吗?为什么?
  2. 流式优化的是”人的等待体验”还是”真实完成时间”?它凭什么能帮你在成本上”止损”?
  3. agent 的三道安全护栏(最小权限 / 沙箱 / HITL)分别防的是模型的哪类错?有没有哪道墙单独防不住、需要另一道补?

【参考答案】

  1. 不该无条件重放,而要先校验这个未执行决策是否还有效、执行它是否安全。因为”模型决定调工具”这个意图基于崩溃前那一刻的上下文;崩溃期间外部可能变了(文件被别人改了、API 已返回)。正确做法:把”上次要调 X”作为待确认项,重新把当前真实状态喂给模型,让它决定”还调不调、参数还对不对”。这对应后端”幂等与状态机”思想——恢复后不盲目重放动作,先校验前置条件。盲目重放”发邮件/删文件”的决策,可能酿成重复副作用,所以未落地的决策要谨慎,已落地的副作用要对齐。
  2. 流式优化的是等待体验,不是真实完成时间——任务该十秒还是十秒。但它和”可打断”绑定后就能止损:agent 跑偏了,憋大段时你干等四十秒才发现方向错,那四十秒的 token 和工具调用钱已花退不回;流式下你三秒看到”它要删生产库”,立刻 Ctrl-C,后面本要烧的 token 根本没产生。可打断 = 及时止损 = 真金白银,等于后端”请求超时主动 cancel 下游”省资源,只不过这里”下游”是模型自己。
  3. 最小权限防”它根本不该有的能力被滥用”(没给生产库账号就不可能 DROP);沙箱防”它有了能力但搞砸了,爆炸半径被圈住”(容器内删错文件重启即恢复);HITL 防”它确实有权、动作不可逆、但这次判断错了”(删库前你喊停)。没有哪道万能:最小权限防不住”你不得不给、但它用错”的场景(必须写文件却写错位置),靠沙箱限爆炸半径加 HITL 写前确认;HITL 也防不住”你疲劳随手点 yes”,靠最小权限和沙箱兜底。所以必须纵深——单层都会被某种错击穿,三层叠起来才没有单点失效,和你后端”认证/授权/隔离/审计”要齐全同理。