id: tool-react-over-text
module: A
week: 2
title: "工具调用范式①:react-over-text(把工具调用当散文手写 JSON)"

concept:
  textbook: |
    ReAct(Yao et al., 2022)让 LLM 在 Thought → Action → Observation 之间交替:
    模型先"想"(Thought),再发一个"动作"(Action),环境执行后把结果(Observation)
    回灌,如此循环直到给出答案。

    最朴素的工程实现是 **react-over-text**:不依赖模型原生的工具调用能力,而是要求
    模型把 Action 以一段约定格式的文本写出来(通常是一段 JSON),平台再用正则把这段
    文本"抠"出来、自己去执行。模型在这里并不"调用"工具,它只是在"描述"一个调用。
  key_point: >
    react-over-text 把工具调用降格成"生成文本 → 正则解析 JSON",是 0 执行 /
    格式飘移 / path-vs-file_path 不一致的总根源。它适合做 fallback,不该当主车道。

code:
  - file: lambdagent/src/lambdagent/fromconfig/compiler.py
    symbol: _compile_react
    lines: "878-933"
    note: >
      文本车道主入口。顶部做门控分叉:react.nativeToolCalls 关闭时(默认),
      返回基于 ConversationLam 的文本 ReAct 循环,模型输出靠正则解析。
  - file: lambdagent/src/lambdagent/conversation.py
    symbol: ConversationLam
    lines: "60-126"
    note: >
      文本车道的"思考"算子。每步把历史 + 最新 observation 拼成 prompt 发给 provider,
      拿回纯文本,再交给上层正则解析出 action。靠 max_input_chars 兜住上下文膨胀。

formal:
  term: "Loop(ConversationLam ∘ action_parser, cond, max_steps)"
  effect: "IO[tool] ⊕ Partial"
  note: >
    action_parser 是一个**部分函数**:模型文本不合约定时解析失败(返回空动作或抛错)。
    这个"部分性(Partial)"正是范式②原生 FC 要从类型上消除的东西——FC 让工具调用
    成为结构化、类型受控的对象,而非一段可能解析失败的散文。

live_demo:
  run: agentexample/research67/workspace/run_20260520_004928
  highlight: >
    999 步 react_step,工具调用全部以文本形式嵌在每步 output 里(看 "[Step N] X done")。
    这是 react-over-text 在长任务里空转/重做的典型形态:步数巨大,真正落盘寥寥。

related:
  - tool-native-fc
  - tool-claude-code-native

takeaway: |
  对照三栏后学生应能说清:
  1. 概念上,react-over-text 仍是合法的 ReAct,只是 Action 用文本承载;
  2. 代码上,它落在 _compile_react 的"非 nativeToolCalls"分支 + ConversationLam;
  3. 形式上,它引入了一个部分函数 action_parser,把"解析失败"这一失效模式带进了系统。
  这就是"为什么需要形式化"的第一个实证:范式②用类型把这个失效模式提前消除。
