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

涉及源码 :src/query_engine.py、src/runtime.py、src/main.py;Rust rust/crates/tools/src/lib.rs(StructuredOutput 工具);对照 rust/crates/claw-cli/src/app.rs(OutputFormat,与 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_message 里 summary_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_message 在 message_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 1、stop_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_json → serde_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」。


相关推荐
yi0114 小时前
LeetCode 219:存在重复元素 II——哈希表记录“最近一次出现的位置”
数据结构·人工智能·笔记·python·算法·leetcode·哈希表
百数平台7 小时前
百数AI文本知识库配置详解:本地文档上传与自定义录入、解析分段策略全说明
低代码·ai
灯澜忆梦7 小时前
【面向对象编程C++】| 基础语法
java·c++·算法
小蒋观天下7 小时前
专项方案:大场景港口AI安防、多干扰环境下的算法调优与落地实操
人工智能·深度学习·算法·安全·机器学习·计算机视觉·ai大模型
Evand J7 小时前
【电机滤波例程5】渐消因子自适应扩展卡尔曼滤波(EKF),MATLAB。PMSM负载突变下的状态估计。附下载链接
算法·matlab
H.莓飛7 小时前
【数据结构】二叉树_OJ题
linux·开发语言·数据结构·算法
百数平台8 小时前
百数AI对话流(Chatflow)开发指南:会话历史配置、角色面板、开场白预置问题与智能体挂载
低代码·ai
好想像大佬一样能够ak所有8 小时前
Leetcode35.搜索插入位置
数据结构·算法·leetcode
aqiu1111118 小时前
【C++算法打怪专栏】LeetCode 165. 比较版本号(双指针与字符串流法)
c++·算法·leetcode
黄金龙PLUS8 小时前
如何基于置换算法来设计对称加密算法
算法·网络安全·密码学·哈希算法·同态加密