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

涉及源码src/query_engine.pysrc/runtime.pysrc/main.py;Rust rust/crates/tools/src/lib.rsStructuredOutput 工具);对照 rust/crates/claw-cli/src/app.rsOutputFormat,与 Python 开关不同名同源问题)。


1. 术语:仓库里至少有三条「结构化」线

机制 位置 structured_output 的关系
QueryEngineConfig.structured_output Python query_engine 本文主角 :决定 port 的 TurnResult.output多行文本 还是 整段缩进 JSON 字符串
StructuredOutput 工具 Rust tools 无关:模型通过工具调用提交任意 JSON 对象,执行器原样包进结果 JSON
OutputFormat::Json / Ndjson Rust claw-cli app.rs 无关:REPL/CLI 对人类可读回复再包一层 JSON 行

下面分述主角与旁支,避免读源码时混为一谈。


2. Python:structured_output 如何改变可解析性

2.1 配置项

python 复制代码
# 15:21:src/query_engine.py
@dataclass(frozen=True)
class QueryEngineConfig:
    max_turns: int = 8
    max_budget_tokens: int = 2000
    compact_after_turns: int = 12
    structured_output: bool = False
    structured_retry_limit: int = 2
  • structured_output=False(默认)submit_messagesummary_lines'\n'.join(...) 拼成 多行纯文本 。对下游而言是 非 JSON :整段 stdout 不能 json.loads 一次吃干净(除非自行截取)。
  • structured_output=True :同一组 summary_lines 被包进对象 {"summary": [...], "session_id": "..."},再 json.dumps(..., indent=2)单文档合法 JSON 字符串(在序列化成功的前提下)。
python 复制代码
# 152:159:src/query_engine.py
    def _format_output(self, summary_lines: list[str]) -> str:
        if self.config.structured_output:
            payload = {
                'summary': summary_lines,
                'session_id': self.session_id,
            }
            return self._render_structured_output(payload)
        return '\n'.join(summary_lines)

2.2 流式事件中的位置

stream_submit_messagemessage_delta 里输出的 text 就是 submit_message 算出的 result.output,因此开关 同时改变 delta 载荷的可解析性

python 复制代码
# 120:121:src/query_engine.py
        result = self.submit_message(prompt, matched_commands, matched_tools, denied_tools)
        yield {'type': 'message_delta', 'text': result.output}

message_stop 里的 usage / stop_reason 仍是结构化 dict(Python 字面量),与 structured_output 无关。

2.3 CLI 打印形态

turn-loop 子命令把 result.output 原样 print ,前面只加 markdown 风格标题;不会再包一层 JSON:

python 复制代码
# 153:158:src/main.py
    if args.command == 'turn-loop':
        results = PortRuntime().run_turn_loop(args.prompt, limit=args.limit, max_turns=args.max_turns, structured_output=args.structured_output)
        for idx, result in enumerate(results, start=1):
            print(f'## Turn {idx}')
            print(result.output)
            print(f'stop_reason={result.stop_reason}')

因此:

  • 开启 structured_output 时,单轮 body 是一段可 json.loads 的字符串,但 整段 stdout 仍含 ## Turn 1stop_reason=...------整体不是单一 JSON 。要做管道解析需 按节切片 或只解析 print(result.output) 那一段。
  • 关闭时,body 为自然语言行,明确非 JSON

3. 失败语义与重试:structured_retry_limit

3.1 重试只针对 json.dumps

python 复制代码
# 161:169:src/query_engine.py
    def _render_structured_output(self, payload: dict[str, object]) -> str:
        last_error: Exception | None = None
        for _ in range(self.config.structured_retry_limit):
            try:
                return json.dumps(payload, indent=2)
            except (TypeError, ValueError) as exc:  # pragma: no cover - defensive branch
                last_error = exc
                payload = {'summary': ['structured output retry'], 'session_id': self.session_id}
        raise RuntimeError('structured output rendering failed') from last_error

语义要点:

  1. 触发条件 :当前 payload 无法被 json.dumps(例如 summary_lines 里混入了不可 JSON 化的对象------在正常路径里 summary_lines 全是 str,该分支标注为 pragma: no cover,属防御性)。
  2. 重试行为 重试同一 payload;而是 替换 为极小安全 payload(固定一句 'structured output retry' + session_id),再试,最多 structured_retry_limit 次。
  3. 最终失败 :抛出 RuntimeError('structured output rendering failed') ,链上保留最后一次 TypeError/ValueError。这是对 调用方 的硬失败;submit_message 没有 try/except 包住 _format_output,故异常会 冒泡TurnResult 不会生成。
  4. 与业务 stop_reason 无关max_turns / max_budget_reached 等仍走正常 TurnResult序列化失败异常路径 ,不是 stop_reason

3.2 早退路径与可解析性不一致

max_turns 已达 时,output 是直接格式化的 英文句子_format_output,因此 即使 structured_output=True,该轮也不会是 JSON

python 复制代码
# 68:77:src/query_engine.py
        if len(self.mutable_messages) >= self.config.max_turns:
            output = f'Max turns reached before processing prompt: {prompt}'
            return TurnResult(
                prompt=prompt,
                output=output,
                ...
                stop_reason='max_turns_reached',
            )

结论structured_output 只保证「正常摘要路径」下的 body 形态;错误/早退字符串 仍可能破坏「每轮皆可 json.loads」的假设------企业若要做严格 JSON 流水线,需在下游 stop_reason 分支统一包装 envelope


4. Rust:StructuredOutput 工具(与配置开关无关)

工具定义允许 任意额外字段additionalProperties: true),执行器把输入 map echo 到 structured_output 字段:

rust 复制代码
// 496:504:rust/crates/tools/src/lib.rs
        ToolSpec {
            name: "StructuredOutput",
            description: "Return structured output in the requested format.",
            input_schema: json!({
                "type": "object",
                "additionalProperties": true
            }),
            required_permission: PermissionMode::ReadOnly,
        },
2536:2541:rust/crates/tools/src/lib.rs 复制代码
fn execute_structured_output(input: StructuredOutputInput) -> StructuredOutputResult {
    StructuredOutputResult {
        data: String::from("Structured output provided successfully"),
        structured_output: input.0,
    }
}

序列化走 to_pretty_jsonserde_json 失败则 Result::Err(String)没有 Python 那种「降级 payload + 多次重试」。失败语义是 工具调用失败,由上层 runtime/对话环处理。

这与 QueryEngineConfig.structured_output 完全独立:前者是 agent 工具链 的契约,后者是 Python port 的演示/测试输出格式


5. 对照:CLI OutputFormat(Rust)

claw-cli 在写出 turn 结果时按 OutputFormat 选择文本或 JSON 行,例如:

rust 复制代码
// 270:288:rust/crates/claw-cli/src/app.rs
        match self.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_output=False structured_output=True
可解析性 多行文本;无 JSON 保证 正常路径下 body 为 单文档 JSON 字符串summary + session_id
流式 message_delta 文本行 同上整段 JSON 字符串
整页 stdout(turn-loop) 非纯 JSON 仍含标题与 stop_reason=非单文件 JSON
序列化失败 不适用 降级 payload 重试 structured_retry_limit 次,再失败 RuntimeError
max_turns 早退 纯文本句子 仍为纯文本,与开关不一致
budget stop_reason 正常 JSON/文本 body 后仍带 max_budget_reached 同左(body 形态仍由 _format_output 决定)

设计启示 :若企业需要 稳定、可机器校验的每轮 envelope ,仅靠当前 structured_output 不够(早退、CLI 装饰行、混用测试)。更稳妥的是在 边界层 (HTTP/SSE、子进程协议)定义 固定顶层 JSON,把人类可读段落放进字段,而不是依赖「整段 print 即 JSON」。


相关推荐
tankeven2 小时前
HJ172 小红的矩阵染色
c++·算法
2301_822703202 小时前
Flutter 框架跨平台鸿蒙开发 - 智能植物生长记录应用
算法·flutter·华为·harmonyos·鸿蒙
每日任务(希望进OD版)2 小时前
线性DP、区间DP
开发语言·数据结构·c++·算法·动态规划
放羊郎2 小时前
机器人跟随算法
算法·机器人
Agent产品评测局2 小时前
企业生产报工自动化落地,数据采集全流程实现方案 —— 2026制造业数字化转型深度选型指南
运维·人工智能·ai·chatgpt·自动化
liu****2 小时前
第十五届蓝桥杯大赛软件赛国赛C/C++大学B组
c++·算法·蓝桥杯·acm
We་ct2 小时前
LeetCode 172. 阶乘后的零:从暴力到最优,拆解解题核心
开发语言·前端·javascript·算法·leetcode·typescript
轻微的风格艾丝凡2 小时前
三相不平衡电流调试经验记录
算法·dsp
小程故事多_802 小时前
AI Coding 工程化革命,Superpowers 管流程,ui-ux-pro-max 管质感
人工智能·ui·架构·aigc·ai编程·ux·claude code