claw-code 源码分析:结构化输出与重试——`structured_output` 一类开关如何改变「可解析性」与失败语义?

张开发
2026/4/12 6:28:49 15 分钟阅读

分享文章

claw-code 源码分析:结构化输出与重试——`structured_output` 一类开关如何改变「可解析性」与失败语义?
涉及源码src/query_engine.py、src/runtime.py、src/main.pyRustrust/crates/tools/src/lib.rsStructuredOutput工具对照rust/crates/claw-cli/src/app.rsOutputFormat与 Python 开关不同名同源问题。1. 术语仓库里至少有三条「结构化」线机制位置与structured_output的关系QueryEngineConfig.structured_outputPythonquery_engine本文主角决定 port 的TurnResult.output是多行文本还是整段缩进 JSON 字符串StructuredOutput工具Rusttools无关模型通过工具调用提交任意 JSON 对象执行器原样包进结果 JSONOutputFormat::Json/NdjsonRustclaw-cliapp.rs无关REPL/CLI 对人类可读回复再包一层 JSON 行下面分述主角与旁支避免读源码时混为一谈。2. Pythonstructured_output如何改变可解析性2.1 配置项# 15:21:src/query_engine.pydataclass(frozenTrue)classQueryEngineConfig:max_turns:int8max_budget_tokens:int2000compact_after_turns:int12structured_output:boolFalsestructured_retry_limit:int2structured_outputFalse默认submit_message里summary_lines用\n.join(...)拼成多行纯文本。对下游而言是非 JSON整段 stdout 不能json.loads一次吃干净除非自行截取。structured_outputTrue同一组summary_lines被包进对象{summary: [...], session_id: ...}再json.dumps(..., indent2)→单文档合法 JSON 字符串在序列化成功的前提下。# 152:159:src/query_engine.pydef_format_output(self,summary_lines:list[str])-str:ifself.config.structured_output:payload{summary:summary_lines,session_id:self.session_id,}returnself._render_structured_output(payload)return\n.join(summary_lines)2.2 流式事件中的位置stream_submit_message在message_delta里输出的text就是submit_message算出的result.output因此开关同时改变 delta 载荷的可解析性# 120:121:src/query_engine.pyresultself.submit_message(prompt,matched_commands,matched_tools,denied_tools)yield{type:message_delta,text:result.output}message_stop里的usage/stop_reason仍是结构化 dictPython 字面量与structured_output无关。2.3 CLI 打印形态turn-loop子命令把result.output原样 print前面只加 markdown 风格标题不会再包一层 JSON# 153:158:src/main.pyifargs.commandturn-loop:resultsPortRuntime().run_turn_loop(args.prompt,limitargs.limit,max_turnsargs.max_turns,structured_outputargs.structured_output)foridx,resultinenumerate(results,start1):print(f## Turn{idx})print(result.output)print(fstop_reason{result.stop_reason})因此开启structured_output时单轮 body是一段可json.loads的字符串但整段 stdout仍含## Turn 1、stop_reason...——整体不是单一 JSON。要做管道解析需按节切片或只解析print(result.output)那一段。关闭时body 为自然语言行明确非 JSON。3. 失败语义与重试structured_retry_limit3.1 重试只针对json.dumps# 161:169:src/query_engine.pydef_render_structured_output(self,payload:dict[str,object])-str:last_error:Exception|NoneNonefor_inrange(self.config.structured_retry_limit):try:returnjson.dumps(payload,indent2)except(TypeError,ValueError)asexc:# pragma: no cover - defensive branchlast_errorexc payload{summary:[structured output retry],session_id:self.session_id}raiseRuntimeError(structured output rendering failed)fromlast_error语义要点触发条件当前 payload 无法被json.dumps例如summary_lines里混入了不可 JSON 化的对象——在正常路径里summary_lines全是str该分支标注为pragma: no cover属防御性。重试行为不重试同一 payload而是替换为极小安全 payload固定一句structured output retrysession_id再试最多structured_retry_limit次。最终失败抛出RuntimeError(structured output rendering failed)链上保留最后一次TypeError/ValueError。这是对调用方的硬失败submit_message没有try/except 包住_format_output故异常会冒泡TurnResult不会生成。与业务stop_reason无关max_turns/max_budget_reached等仍走正常TurnResult序列化失败是异常路径不是stop_reason。3.2 早退路径与可解析性不一致在max_turns已达时output是直接格式化的英文句子不走_format_output因此即使structured_outputTrue该轮也不会是 JSON# 68:77:src/query_engine.pyiflen(self.mutable_messages)self.config.max_turns:outputfMax turns reached before processing prompt:{prompt}returnTurnResult(promptprompt,outputoutput,...stop_reasonmax_turns_reached,)结论structured_output只保证「正常摘要路径」下的 body 形态错误/早退字符串仍可能破坏「每轮皆可json.loads」的假设——企业若要做严格 JSON 流水线需在下游按stop_reason分支或统一包装 envelope。4. RustStructuredOutput工具与配置开关无关工具定义允许任意额外字段additionalProperties: true执行器把输入 map echo 到structured_output字段// 496:504:rust/crates/tools/src/lib.rsToolSpec{name:StructuredOutput,description:Return structured output in the requested format.,input_schema:json!({type:object,additionalProperties:true}),required_permission:PermissionMode::ReadOnly,},fn execute_structured_output(input: StructuredOutputInput) - StructuredOutputResult { StructuredOutputResult { data: String::from(Structured output provided successfully), structured_output: input.0, } }序列化走to_pretty_json→serde_json失败则Result::Err(String)没有Python 那种「降级 payload 多次重试」。失败语义是工具调用失败由上层 runtime/对话环处理。这与QueryEngineConfig.structured_output完全独立前者是agent 工具链的契约后者是Python port 的演示/测试输出格式。5. 对照CLIOutputFormatRustclaw-cli在写出 turn 结果时按OutputFormat选择文本或 JSON 行例如// 270:288:rust/crates/claw-cli/src/app.rsmatchself.config.output_format{OutputFormat::Text{writeln!(out,\nToken usage: {} input / {} output,self.state.last_usage.input_tokens,self.state.last_usage.output_tokens)?;}OutputFormat::Json{writeln!(out,{},serde_json::json!({message:summary.assistant_text,usage:{input_tokens:self.state.last_usage.input_tokens,output_tokens:self.state.last_usage.output_tokens,}}))?;}这里JSON 由代码构造不依赖对模型输出做json.dumps失败主要来自 IO而非「内容不可序列化」。与 Python port 的structured_output 重试是不同问题域。6. 小结表维度structured_outputFalsestructured_outputTrue可解析性多行文本无 JSON 保证正常路径下 body 为单文档 JSON 字符串summarysession_id流式message_delta文本行同上整段 JSON 字符串整页 stdoutturn-loop非纯 JSON仍含标题与stop_reason非单文件 JSON序列化失败不适用降级 payload 重试structured_retry_limit次再失败RuntimeErrormax_turns 早退纯文本句子仍为纯文本与开关不一致budgetstop_reason正常 JSON/文本 body 后仍带max_budget_reached同左body 形态仍由_format_output决定设计启示若企业需要稳定、可机器校验的每轮 envelope仅靠当前structured_output不够早退、CLI 装饰行、混用测试。更稳妥的是在边界层HTTP/SSE、子进程协议定义固定顶层 JSON把人类可读段落放进字段而不是依赖「整段 print 即 JSON」。

更多文章