ReAct(Yao et al., 2022)让 LLM 在 Thought → Action → Observation 之间交替: 模型先"想"(Thought),再发一个"动作"(Action),环境执行后把结果(Observation) 回灌,如此循环直到给出答案。 最朴素的工程实现是 **react-over-text**:不依赖模型原生的工具调用能力,而是要求 模型把 Action 以一段约定格式的文本写出来(通常是一段 JSON),平台再用正则把这段 文本"抠"出来、自己去执行。模型在这里并不"调用"工具,它只是在"描述"一个调用。
878def _compile_react(cfg: Dict, overrides: Dict) -> Term:879 """880 type: react -> Loop(react_step, condition, max_steps)881882 v2 optimizations:883 - Context passed through via closure over shared ctx884 - Sliding window state compression885 - Early termination on implicit signals886 - Tool call caching887 """888 # Inject tool parameter docs into system prompt889 cfg = dict(cfg) # shallow copy to avoid mutating original890 tool_docs = _generate_tool_schema_docs(cfg)891 if tool_docs:892 cfg["systemPrompt"] = cfg.get("systemPrompt", "") + tool_docs893894 # 可选:磁盘进度纪律(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_FRAGMENT899900 think = _compile_lam(cfg, "think", overrides=overrides)901 tools = _compile_tools(cfg, overrides)902903 # S05: Enforce mcp.policy.mode at runtime904 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 execution910 # "auto" and "intelligence" are default behavior (LLM decides)911912 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"))918919 # ── 原生 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_cancel930 except Exception:931 _fc_cancel = None932 # enforceLoop 计数模式(单体 agent 用):terminate 前 tool 须调够 minCount 次。933 # 序列模式(orchestrator)FC 暂不支持——那些 agent 不设 nativeToolCalls。60class ConversationLam(Term):61 """62 Lambda abstraction with conversation persistence.6364 Lambda semantics preserved:65 ConversationLam(provider, prompt) = lambda x. provider(history + x)66 apply() = beta-reduction with memory6768 The key difference from stateless Lam:69 Lam: each apply() is independent70 ConversationLam: each apply() builds on all previous calls7172 L03: Now supports both legacy dict-based and ChatMessage-based flows.73 """7475 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 params84 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 fixed89 # CHARACTER count (qwen: 258048 → "Range of input length should be90 # [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 as92 # 320k chars sails past the char limit. We keep a conservative margin93 # below 258048 for the system prompt + the next user turn.94 max_input_chars: int = 200000,95 ):96 super().__init__(name)97 self.provider = provider98 self.system_prompt = system_prompt99 self.max_history_tokens = max_history_tokens100 self.keep_recent_turns = keep_recent_turns101 self.max_input_chars = max_input_chars102 self.output_parser = output_parser or (lambda x: x)103 # L03: Store model/temperature/max_tokens for chat_typed calls104 self._model = model105 self._temperature = temperature106 self._max_tokens = max_tokens107108 # Conversation history (system message always first)109 self.messages: List[dict] = [110 {"role": "system", "content": system_prompt}111 ]112113 # L03: ChatMessage-based history (parallel to dict-based for typed path)114 self._typed_messages: List[ChatMessage] = []115116 # Expose model name for react_step logging117 @property118 def model(self) -> str:119 return self._model or self.provider.model_name120121 # Expose _session_id for react_step session detection122 @property123 def _session_id(self):124 return getattr(self.provider, '_session_id', None)125126 def apply(self, input: Any, ctx: Context | None = None) -> Any:action_parser 是一个**部分函数**:模型文本不合约定时解析失败(返回空动作或抛错)。 这个"部分性(Partial)"正是范式②原生 FC 要从类型上消除的东西——FC 让工具调用 成为结构化、类型受控的对象,而非一段可能解析失败的散文。
agentexample/research67/workspace/run_20260520_004928 · status=completed999 步 react_step,工具调用全部以文本形式嵌在每步 output 里(看 "[Step N] X done")。 这是 react-over-text 在长任务里空转/重做的典型形态:步数巨大,真正落盘寥寥。
相关对照卡: 工具调用范式②:原生 function-calling(结构化 tool_calls) 工具调用范式③:claude-code 原生车道(把工具调用外包给运行时)