← 对照卡索引
模块 A第 2 周 tool-react-over-text

工具调用范式①:react-over-text(把工具调用当散文手写 JSON)

🎯 react-over-text 把工具调用降格成"生成文本 → 正则解析 JSON",是 0 执行 / 格式飘移 / path-vs-file_path 不一致的总根源。它适合做 fallback,不该当主车道。

📖 教科书概念

ReAct(Yao et al., 2022)让 LLM 在 Thought → Action → Observation 之间交替: 模型先"想"(Thought),再发一个"动作"(Action),环境执行后把结果(Observation) 回灌,如此循环直到给出答案。 最朴素的工程实现是 **react-over-text**:不依赖模型原生的工具调用能力,而是要求 模型把 Action 以一段约定格式的文本写出来(通常是一段 JSON),平台再用正则把这段 文本"抠"出来、自己去执行。模型在这里并不"调用"工具,它只是在"描述"一个调用。

💻 平台源码落点

lambdagent/src/lambdagent/fromconfig/compiler.py · _compile_react · L878-933
878def _compile_react(cfg: Dict, overrides: Dict) -> Term:
879 """
880 type: react -> Loop(react_step, condition, max_steps)
881
882 v2 optimizations:
883 - Context passed through via closure over shared ctx
884 - Sliding window state compression
885 - Early termination on implicit signals
886 - Tool call caching
887 """
888 # Inject tool parameter docs into system prompt
889 cfg = dict(cfg) # shallow copy to avoid mutating original
890 tool_docs = _generate_tool_schema_docs(cfg)
891 if tool_docs:
892 cfg["systemPrompt"] = cfg.get("systemPrompt", "") + tool_docs
893
894 # 可选:磁盘进度纪律(react.progressDiscipline)。把 PROGRESS.md 待办纪律注入 systemPrompt,
895 # 防多步任务"上下文截断→失忆→工具调用乱跳/重做"。默认 off,只对显式开启的 react agent 注入;
896 # simple 子智能体走 _compile_lam 不走这里,天然不受影响。见 docs/PROGRESS_DISCIPLINE_DESIGN.md。
897 if (cfg.get("react", {}) or {}).get("progressDiscipline"):
898 cfg["systemPrompt"] = cfg.get("systemPrompt", "") + _PROGRESS_DISCIPLINE_FRAGMENT
899
900 think = _compile_lam(cfg, "think", overrides=overrides)
901 tools = _compile_tools(cfg, overrides)
902
903 # S05: Enforce mcp.policy.mode at runtime
904 mcp_cfg = cfg.get("mcp", {})
905 mcp_policy_mode = mcp_cfg.get("policy", {}).get("mode", "auto")
906 if mcp_policy_mode == "disable":
907 tools = {"terminate": tools.get("terminate", Tool("terminate", fn=lambda x: x))}
908 elif mcp_policy_mode == "force":
909 pass # All tools available, forced execution
910 # "auto" and "intelligence" are default behavior (LLM decides)
911
912 react_cfg = cfg.get("react", {})
913 max_steps = react_cfg.get("maxSteps", 10)
914 tool_timeout = react_cfg.get("toolTimeout", 30)
915 observation_enabled = react_cfg.get("observationEnabled", True)
916 verbose = react_cfg.get("verbose", False)
917 agent_name = cfg.get("name", cfg.get("agentId", "agent"))
918
919 # ── 原生 function-calling 车道(react.nativeToolCalls,阶段 2)──
920 # FC-capable provider(OpenAI 兼容:qwen/openai/deepseek…)+ 开关 → 用结构化
921 # tool_calls 直接执行,根治"文本求 JSON、正则抠"那一类 0 执行/格式飘 bug。
922 # 旧文本路径(下方)保留作 fallback:provider 不支持 FC 或开关 off 时走它。
923 # 见 docs/NATIVE_FUNCTION_CALLING_DESIGN.md。
924 if react_cfg.get("nativeToolCalls"):
925 _prov = getattr(think, "provider", None)
926 if (_prov is not None and hasattr(_prov, "chat_with_tools")
927 and getattr(_prov, "supports_function_calling", lambda: False)()):
928 try:
929 from lambdagent.agentruntime import cancel as _fc_cancel
930 except Exception:
931 _fc_cancel = None
932 # enforceLoop 计数模式(单体 agent 用):terminate 前 tool 须调够 minCount 次。
933 # 序列模式(orchestrator)FC 暂不支持——那些 agent 不设 nativeToolCalls。
文本车道主入口。顶部做门控分叉:react.nativeToolCalls 关闭时(默认), 返回基于 ConversationLam 的文本 ReAct 循环,模型输出靠正则解析。
lambdagent/src/lambdagent/conversation.py · ConversationLam · L60-126
60class ConversationLam(Term):
61 """
62 Lambda abstraction with conversation persistence.
63
64 Lambda semantics preserved:
65 ConversationLam(provider, prompt) = lambda x. provider(history + x)
66 apply() = beta-reduction with memory
67
68 The key difference from stateless Lam:
69 Lam: each apply() is independent
70 ConversationLam: each apply() builds on all previous calls
71
72 L03: Now supports both legacy dict-based and ChatMessage-based flows.
73 """
74
75 def __init__(
76 self,
77 name: str,
78 provider: LLMProvider,
79 system_prompt: str,
80 max_history_tokens: int = 80000,
81 keep_recent_turns: int = 20,
82 output_parser: Callable[[str], Any] | None = None,
83 # L03: New unified interface params
84 model: str = "",
85 temperature: float = 0.0,
86 max_tokens: int = 4096,
87 # Hard character ceiling for the *rendered* request (system + history +
88 # new input). Providers like DashScope/qwen reject inputs over a fixed
89 # CHARACTER count (qwen: 258048 → "Range of input length should be
90 # [1, 258048]"). The token-based budget above is not enough on its own:
91 # for CJK text 1 token ≈ 1 char, so an 80k-"token" history estimated as
92 # 320k chars sails past the char limit. We keep a conservative margin
93 # below 258048 for the system prompt + the next user turn.
94 max_input_chars: int = 200000,
95 ):
96 super().__init__(name)
97 self.provider = provider
98 self.system_prompt = system_prompt
99 self.max_history_tokens = max_history_tokens
100 self.keep_recent_turns = keep_recent_turns
101 self.max_input_chars = max_input_chars
102 self.output_parser = output_parser or (lambda x: x)
103 # L03: Store model/temperature/max_tokens for chat_typed calls
104 self._model = model
105 self._temperature = temperature
106 self._max_tokens = max_tokens
107
108 # Conversation history (system message always first)
109 self.messages: List[dict] = [
110 {"role": "system", "content": system_prompt}
111 ]
112
113 # L03: ChatMessage-based history (parallel to dict-based for typed path)
114 self._typed_messages: List[ChatMessage] = []
115
116 # Expose model name for react_step logging
117 @property
118 def model(self) -> str:
119 return self._model or self.provider.model_name
120
121 # Expose _session_id for react_step session detection
122 @property
123 def _session_id(self):
124 return getattr(self.provider, '_session_id', None)
125
126 def apply(self, input: Any, ctx: Context | None = None) -> Any:
文本车道的"思考"算子。每步把历史 + 最新 observation 拼成 prompt 发给 provider, 拿回纯文本,再交给上层正则解析出 action。靠 max_input_chars 兜住上下文膨胀。

∑ 形式化对象

Loop(ConversationLam ∘ action_parser, cond, max_steps)
effect: IO[tool] ⊕ Partial

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

🎞 Live Demo · 真实 run

agentexample/research67/workspace/run_20260520_004928 · status=completed
999
0 tokens
18535.7 s
ReadFile ×998WriteFile ×998

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

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

相关对照卡: 工具调用范式②:原生 function-calling(结构化 tool_calls) 工具调用范式③:claude-code 原生车道(把工具调用外包给运行时)