tool-react-over-text.html 22 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. <!doctype html><html lang="zh"><head><meta charset="utf-8">
  2. <meta name="viewport" content="width=device-width,initial-scale=1">
  3. <title>工具调用范式①:react-over-text(把工具调用当散文手写 JSON)</title><style>
  4. :root{--fg:#1a1a1a;--bg:#fff;--mut:#666;--line:#e3e3e3;--accent:#3b5bdb;
  5. --codebg:#f6f8fa;--warn:#b00020;--okbg:#eef4ff;--formalbg:#f3f0ff;--conceptbg:#fbfbf9;}
  6. *{box-sizing:border-box}
  7. body{font:15px/1.65 -apple-system,"PingFang SC",Segoe UI,Roboto,sans-serif;
  8. color:var(--fg);background:var(--bg);margin:0;padding:0}
  9. a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}
  10. .wrap{max-width:1180px;margin:0 auto;padding:28px 22px 80px}
  11. header.top{border-bottom:1px solid var(--line);padding-bottom:14px;margin-bottom:22px}
  12. header.top h1{margin:0 0 4px;font-size:20px}
  13. .sub{color:var(--mut);font-size:13px}
  14. .crumbs{font-size:13px;margin-bottom:18px}.crumbs a{color:var(--mut)}
  15. .badge{display:inline-block;font-size:12px;background:var(--okbg);color:var(--accent);
  16. border-radius:4px;padding:1px 8px;margin-right:6px}
  17. h2.ctitle{font-size:19px;margin:6px 0 4px}
  18. .keypoint{background:#fff8e6;border-left:3px solid #e6a700;padding:10px 14px;
  19. border-radius:4px;margin:14px 0;font-size:14px}
  20. .cols{display:grid;grid-template-columns:1fr 1.5fr 1fr;gap:16px;margin:22px 0}
  21. .col{border:1px solid var(--line);border-radius:8px;padding:14px 16px;min-width:0}
  22. .col h3{margin:0 0 10px;font-size:13px;letter-spacing:.04em;text-transform:uppercase;color:var(--mut)}
  23. .col.concept{background:var(--conceptbg)}
  24. .col.formal{background:var(--formalbg)}
  25. .col p{margin:0 0 10px;white-space:pre-wrap;font-size:14px}
  26. .anchor{margin:0 0 16px}
  27. .anchor .meta{font-size:12px;color:var(--mut);margin-bottom:4px}
  28. .anchor .meta code{background:var(--codebg);padding:1px 5px;border-radius:3px}
  29. pre{background:var(--codebg);border:1px solid var(--line);border-radius:6px;
  30. overflow:auto;margin:0 0 6px;padding:10px 0;font-size:12px;line-height:1.5}
  31. pre code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;display:block}
  32. .ln{display:flex}.ln .n{color:#aaa;text-align:right;width:46px;padding:0 10px;
  33. user-select:none;flex:none}.ln .c{white-space:pre;padding-right:14px}
  34. .note{font-size:12.5px;color:var(--mut);margin:0 0 14px}
  35. .warn{color:var(--warn);background:#fff0f0;border:1px solid #ffd0d0;border-radius:6px;
  36. padding:8px 12px;font-size:13px}
  37. .kv{font-size:13px}.kv code{background:var(--codebg);padding:1px 5px;border-radius:3px}
  38. .formal .term{font-family:ui-monospace,Menlo,monospace;background:#fff;border:1px solid var(--line);
  39. border-radius:5px;padding:8px 10px;font-size:13px;margin-bottom:8px;word-break:break-word}
  40. .effect{font-family:ui-monospace,Menlo,monospace;color:#7048e8;font-size:13px;margin-bottom:8px}
  41. .trace{border:1px solid var(--line);border-radius:8px;padding:14px 16px;margin:22px 0;background:#fafafa}
  42. .trace h3{margin:0 0 10px;font-size:14px}
  43. .stat{display:inline-block;margin:0 18px 6px 0;font-size:13px}
  44. .stat b{font-size:17px;color:var(--accent)}
  45. .toolchip{display:inline-block;background:#eef;border:1px solid #dde;border-radius:12px;
  46. padding:1px 9px;margin:0 6px 6px 0;font-size:12px}
  47. .takeaway{background:var(--okbg);border-radius:8px;padding:14px 18px;margin:22px 0;
  48. white-space:pre-wrap;font-size:14px}
  49. .rel a{margin-right:14px;font-size:13px}
  50. table.idx{border-collapse:collapse;width:100%;margin-top:8px}
  51. table.idx td{border-bottom:1px solid var(--line);padding:9px 8px;vertical-align:top}
  52. table.idx .wk{color:var(--mut);font-size:13px;width:64px;white-space:nowrap}
  53. .modhdr{margin:26px 0 4px;font-size:15px;color:var(--accent)}
  54. footer{margin-top:50px;color:var(--mut);font-size:12px;border-top:1px solid var(--line);padding-top:14px}
  55. @media(max-width:900px){.cols{grid-template-columns:1fr}}
  56. </style></head><body><div class="wrap">
  57. <div class="crumbs"><a href="index.html">← 对照卡索引</a></div>
  58. <header class="top">
  59. <span class="badge">模块 A</span><span class="badge">第 2 周</span>
  60. <span class="badge">tool-react-over-text</span>
  61. <h2 class="ctitle">工具调用范式①:react-over-text(把工具调用当散文手写 JSON)</h2></header>
  62. <div class="keypoint">🎯 react-over-text 把工具调用降格成&quot;生成文本 → 正则解析 JSON&quot;,是 0 执行 / 格式飘移 / path-vs-file_path 不一致的总根源。它适合做 fallback,不该当主车道。
  63. </div>
  64. <div class="cols">
  65. <div class="col concept"><h3>📖 教科书概念</h3><p>ReAct(Yao et al., 2022)让 LLM 在 Thought → Action → Observation 之间交替:
  66. 模型先&quot;想&quot;(Thought),再发一个&quot;动作&quot;(Action),环境执行后把结果(Observation)
  67. 回灌,如此循环直到给出答案。
  68. 最朴素的工程实现是 **react-over-text**:不依赖模型原生的工具调用能力,而是要求
  69. 模型把 Action 以一段约定格式的文本写出来(通常是一段 JSON),平台再用正则把这段
  70. 文本&quot;抠&quot;出来、自己去执行。模型在这里并不&quot;调用&quot;工具,它只是在&quot;描述&quot;一个调用。
  71. </p></div>
  72. <div class="col code"><h3>💻 平台源码落点</h3><div class="anchor"><div class="meta"><code>lambdagent/src/lambdagent/fromconfig/compiler.py</code> · <b>_compile_react</b> · L878-933</div><pre><code><div class="ln"><span class="n">878</span><span class="c">def _compile_react(cfg: Dict, overrides: Dict) -&gt; Term:</span></div><div class="ln"><span class="n">879</span><span class="c"> &quot;&quot;&quot;</span></div><div class="ln"><span class="n">880</span><span class="c"> type: react -&gt; Loop(react_step, condition, max_steps)</span></div><div class="ln"><span class="n">881</span><span class="c"></span></div><div class="ln"><span class="n">882</span><span class="c"> v2 optimizations:</span></div><div class="ln"><span class="n">883</span><span class="c"> - Context passed through via closure over shared ctx</span></div><div class="ln"><span class="n">884</span><span class="c"> - Sliding window state compression</span></div><div class="ln"><span class="n">885</span><span class="c"> - Early termination on implicit signals</span></div><div class="ln"><span class="n">886</span><span class="c"> - Tool call caching</span></div><div class="ln"><span class="n">887</span><span class="c"> &quot;&quot;&quot;</span></div><div class="ln"><span class="n">888</span><span class="c"> # Inject tool parameter docs into system prompt</span></div><div class="ln"><span class="n">889</span><span class="c"> cfg = dict(cfg) # shallow copy to avoid mutating original</span></div><div class="ln"><span class="n">890</span><span class="c"> tool_docs = _generate_tool_schema_docs(cfg)</span></div><div class="ln"><span class="n">891</span><span class="c"> if tool_docs:</span></div><div class="ln"><span class="n">892</span><span class="c"> cfg[&quot;systemPrompt&quot;] = cfg.get(&quot;systemPrompt&quot;, &quot;&quot;) + tool_docs</span></div><div class="ln"><span class="n">893</span><span class="c"></span></div><div class="ln"><span class="n">894</span><span class="c"> # 可选:磁盘进度纪律(react.progressDiscipline)。把 PROGRESS.md 待办纪律注入 systemPrompt,</span></div><div class="ln"><span class="n">895</span><span class="c"> # 防多步任务&quot;上下文截断→失忆→工具调用乱跳/重做&quot;。默认 off,只对显式开启的 react agent 注入;</span></div><div class="ln"><span class="n">896</span><span class="c"> # simple 子智能体走 _compile_lam 不走这里,天然不受影响。见 docs/PROGRESS_DISCIPLINE_DESIGN.md。</span></div><div class="ln"><span class="n">897</span><span class="c"> if (cfg.get(&quot;react&quot;, {}) or {}).get(&quot;progressDiscipline&quot;):</span></div><div class="ln"><span class="n">898</span><span class="c"> cfg[&quot;systemPrompt&quot;] = cfg.get(&quot;systemPrompt&quot;, &quot;&quot;) + _PROGRESS_DISCIPLINE_FRAGMENT</span></div><div class="ln"><span class="n">899</span><span class="c"></span></div><div class="ln"><span class="n">900</span><span class="c"> think = _compile_lam(cfg, &quot;think&quot;, overrides=overrides)</span></div><div class="ln"><span class="n">901</span><span class="c"> tools = _compile_tools(cfg, overrides)</span></div><div class="ln"><span class="n">902</span><span class="c"></span></div><div class="ln"><span class="n">903</span><span class="c"> # S05: Enforce mcp.policy.mode at runtime</span></div><div class="ln"><span class="n">904</span><span class="c"> mcp_cfg = cfg.get(&quot;mcp&quot;, {})</span></div><div class="ln"><span class="n">905</span><span class="c"> mcp_policy_mode = mcp_cfg.get(&quot;policy&quot;, {}).get(&quot;mode&quot;, &quot;auto&quot;)</span></div><div class="ln"><span class="n">906</span><span class="c"> if mcp_policy_mode == &quot;disable&quot;:</span></div><div class="ln"><span class="n">907</span><span class="c"> tools = {&quot;terminate&quot;: tools.get(&quot;terminate&quot;, Tool(&quot;terminate&quot;, fn=lambda x: x))}</span></div><div class="ln"><span class="n">908</span><span class="c"> elif mcp_policy_mode == &quot;force&quot;:</span></div><div class="ln"><span class="n">909</span><span class="c"> pass # All tools available, forced execution</span></div><div class="ln"><span class="n">910</span><span class="c"> # &quot;auto&quot; and &quot;intelligence&quot; are default behavior (LLM decides)</span></div><div class="ln"><span class="n">911</span><span class="c"></span></div><div class="ln"><span class="n">912</span><span class="c"> react_cfg = cfg.get(&quot;react&quot;, {})</span></div><div class="ln"><span class="n">913</span><span class="c"> max_steps = react_cfg.get(&quot;maxSteps&quot;, 10)</span></div><div class="ln"><span class="n">914</span><span class="c"> tool_timeout = react_cfg.get(&quot;toolTimeout&quot;, 30)</span></div><div class="ln"><span class="n">915</span><span class="c"> observation_enabled = react_cfg.get(&quot;observationEnabled&quot;, True)</span></div><div class="ln"><span class="n">916</span><span class="c"> verbose = react_cfg.get(&quot;verbose&quot;, False)</span></div><div class="ln"><span class="n">917</span><span class="c"> agent_name = cfg.get(&quot;name&quot;, cfg.get(&quot;agentId&quot;, &quot;agent&quot;))</span></div><div class="ln"><span class="n">918</span><span class="c"></span></div><div class="ln"><span class="n">919</span><span class="c"> # ── 原生 function-calling 车道(react.nativeToolCalls,阶段 2)──</span></div><div class="ln"><span class="n">920</span><span class="c"> # FC-capable provider(OpenAI 兼容:qwen/openai/deepseek…)+ 开关 → 用结构化</span></div><div class="ln"><span class="n">921</span><span class="c"> # tool_calls 直接执行,根治&quot;文本求 JSON、正则抠&quot;那一类 0 执行/格式飘 bug。</span></div><div class="ln"><span class="n">922</span><span class="c"> # 旧文本路径(下方)保留作 fallback:provider 不支持 FC 或开关 off 时走它。</span></div><div class="ln"><span class="n">923</span><span class="c"> # 见 docs/NATIVE_FUNCTION_CALLING_DESIGN.md。</span></div><div class="ln"><span class="n">924</span><span class="c"> if react_cfg.get(&quot;nativeToolCalls&quot;):</span></div><div class="ln"><span class="n">925</span><span class="c"> _prov = getattr(think, &quot;provider&quot;, None)</span></div><div class="ln"><span class="n">926</span><span class="c"> if (_prov is not None and hasattr(_prov, &quot;chat_with_tools&quot;)</span></div><div class="ln"><span class="n">927</span><span class="c"> and getattr(_prov, &quot;supports_function_calling&quot;, lambda: False)()):</span></div><div class="ln"><span class="n">928</span><span class="c"> try:</span></div><div class="ln"><span class="n">929</span><span class="c"> from lambdagent.agentruntime import cancel as _fc_cancel</span></div><div class="ln"><span class="n">930</span><span class="c"> except Exception:</span></div><div class="ln"><span class="n">931</span><span class="c"> _fc_cancel = None</span></div><div class="ln"><span class="n">932</span><span class="c"> # enforceLoop 计数模式(单体 agent 用):terminate 前 tool 须调够 minCount 次。</span></div><div class="ln"><span class="n">933</span><span class="c"> # 序列模式(orchestrator)FC 暂不支持——那些 agent 不设 nativeToolCalls。</span></div></code></pre><div class="note">文本车道主入口。顶部做门控分叉:react.nativeToolCalls 关闭时(默认), 返回基于 ConversationLam 的文本 ReAct 循环,模型输出靠正则解析。
  73. </div></div><div class="anchor"><div class="meta"><code>lambdagent/src/lambdagent/conversation.py</code> · <b>ConversationLam</b> · L60-126</div><pre><code><div class="ln"><span class="n">60</span><span class="c">class ConversationLam(Term):</span></div><div class="ln"><span class="n">61</span><span class="c"> &quot;&quot;&quot;</span></div><div class="ln"><span class="n">62</span><span class="c"> Lambda abstraction with conversation persistence.</span></div><div class="ln"><span class="n">63</span><span class="c"></span></div><div class="ln"><span class="n">64</span><span class="c"> Lambda semantics preserved:</span></div><div class="ln"><span class="n">65</span><span class="c"> ConversationLam(provider, prompt) = lambda x. provider(history + x)</span></div><div class="ln"><span class="n">66</span><span class="c"> apply() = beta-reduction with memory</span></div><div class="ln"><span class="n">67</span><span class="c"></span></div><div class="ln"><span class="n">68</span><span class="c"> The key difference from stateless Lam:</span></div><div class="ln"><span class="n">69</span><span class="c"> Lam: each apply() is independent</span></div><div class="ln"><span class="n">70</span><span class="c"> ConversationLam: each apply() builds on all previous calls</span></div><div class="ln"><span class="n">71</span><span class="c"></span></div><div class="ln"><span class="n">72</span><span class="c"> L03: Now supports both legacy dict-based and ChatMessage-based flows.</span></div><div class="ln"><span class="n">73</span><span class="c"> &quot;&quot;&quot;</span></div><div class="ln"><span class="n">74</span><span class="c"></span></div><div class="ln"><span class="n">75</span><span class="c"> def __init__(</span></div><div class="ln"><span class="n">76</span><span class="c"> self,</span></div><div class="ln"><span class="n">77</span><span class="c"> name: str,</span></div><div class="ln"><span class="n">78</span><span class="c"> provider: LLMProvider,</span></div><div class="ln"><span class="n">79</span><span class="c"> system_prompt: str,</span></div><div class="ln"><span class="n">80</span><span class="c"> max_history_tokens: int = 80000,</span></div><div class="ln"><span class="n">81</span><span class="c"> keep_recent_turns: int = 20,</span></div><div class="ln"><span class="n">82</span><span class="c"> output_parser: Callable[[str], Any] | None = None,</span></div><div class="ln"><span class="n">83</span><span class="c"> # L03: New unified interface params</span></div><div class="ln"><span class="n">84</span><span class="c"> model: str = &quot;&quot;,</span></div><div class="ln"><span class="n">85</span><span class="c"> temperature: float = 0.0,</span></div><div class="ln"><span class="n">86</span><span class="c"> max_tokens: int = 4096,</span></div><div class="ln"><span class="n">87</span><span class="c"> # Hard character ceiling for the *rendered* request (system + history +</span></div><div class="ln"><span class="n">88</span><span class="c"> # new input). Providers like DashScope/qwen reject inputs over a fixed</span></div><div class="ln"><span class="n">89</span><span class="c"> # CHARACTER count (qwen: 258048 → &quot;Range of input length should be</span></div><div class="ln"><span class="n">90</span><span class="c"> # [1, 258048]&quot;). The token-based budget above is not enough on its own:</span></div><div class="ln"><span class="n">91</span><span class="c"> # for CJK text 1 token ≈ 1 char, so an 80k-&quot;token&quot; history estimated as</span></div><div class="ln"><span class="n">92</span><span class="c"> # 320k chars sails past the char limit. We keep a conservative margin</span></div><div class="ln"><span class="n">93</span><span class="c"> # below 258048 for the system prompt + the next user turn.</span></div><div class="ln"><span class="n">94</span><span class="c"> max_input_chars: int = 200000,</span></div><div class="ln"><span class="n">95</span><span class="c"> ):</span></div><div class="ln"><span class="n">96</span><span class="c"> super().__init__(name)</span></div><div class="ln"><span class="n">97</span><span class="c"> self.provider = provider</span></div><div class="ln"><span class="n">98</span><span class="c"> self.system_prompt = system_prompt</span></div><div class="ln"><span class="n">99</span><span class="c"> self.max_history_tokens = max_history_tokens</span></div><div class="ln"><span class="n">100</span><span class="c"> self.keep_recent_turns = keep_recent_turns</span></div><div class="ln"><span class="n">101</span><span class="c"> self.max_input_chars = max_input_chars</span></div><div class="ln"><span class="n">102</span><span class="c"> self.output_parser = output_parser or (lambda x: x)</span></div><div class="ln"><span class="n">103</span><span class="c"> # L03: Store model/temperature/max_tokens for chat_typed calls</span></div><div class="ln"><span class="n">104</span><span class="c"> self._model = model</span></div><div class="ln"><span class="n">105</span><span class="c"> self._temperature = temperature</span></div><div class="ln"><span class="n">106</span><span class="c"> self._max_tokens = max_tokens</span></div><div class="ln"><span class="n">107</span><span class="c"></span></div><div class="ln"><span class="n">108</span><span class="c"> # Conversation history (system message always first)</span></div><div class="ln"><span class="n">109</span><span class="c"> self.messages: List[dict] = [</span></div><div class="ln"><span class="n">110</span><span class="c"> {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: system_prompt}</span></div><div class="ln"><span class="n">111</span><span class="c"> ]</span></div><div class="ln"><span class="n">112</span><span class="c"></span></div><div class="ln"><span class="n">113</span><span class="c"> # L03: ChatMessage-based history (parallel to dict-based for typed path)</span></div><div class="ln"><span class="n">114</span><span class="c"> self._typed_messages: List[ChatMessage] = []</span></div><div class="ln"><span class="n">115</span><span class="c"></span></div><div class="ln"><span class="n">116</span><span class="c"> # Expose model name for react_step logging</span></div><div class="ln"><span class="n">117</span><span class="c"> @property</span></div><div class="ln"><span class="n">118</span><span class="c"> def model(self) -&gt; str:</span></div><div class="ln"><span class="n">119</span><span class="c"> return self._model or self.provider.model_name</span></div><div class="ln"><span class="n">120</span><span class="c"></span></div><div class="ln"><span class="n">121</span><span class="c"> # Expose _session_id for react_step session detection</span></div><div class="ln"><span class="n">122</span><span class="c"> @property</span></div><div class="ln"><span class="n">123</span><span class="c"> def _session_id(self):</span></div><div class="ln"><span class="n">124</span><span class="c"> return getattr(self.provider, &#x27;_session_id&#x27;, None)</span></div><div class="ln"><span class="n">125</span><span class="c"></span></div><div class="ln"><span class="n">126</span><span class="c"> def apply(self, input: Any, ctx: Context | None = None) -&gt; Any:</span></div></code></pre><div class="note">文本车道的&quot;思考&quot;算子。每步把历史 + 最新 observation 拼成 prompt 发给 provider, 拿回纯文本,再交给上层正则解析出 action。靠 max_input_chars 兜住上下文膨胀。
  74. </div></div></div>
  75. <div class="col formal"><h3>∑ 形式化对象</h3><div class="term">Loop(ConversationLam ∘ action_parser, cond, max_steps)</div><div class="effect">effect: IO[tool] ⊕ Partial</div><p>action_parser 是一个**部分函数**:模型文本不合约定时解析失败(返回空动作或抛错)。 这个&quot;部分性(Partial)&quot;正是范式②原生 FC 要从类型上消除的东西——FC 让工具调用 成为结构化、类型受控的对象,而非一段可能解析失败的散文。
  76. </p></div>
  77. </div>
  78. <div class="trace"><h3>🎞 Live Demo · 真实 run</h3><div class="note"><code>agentexample/research67/workspace/run_20260520_004928</code> · status=completed</div><div class="stat"><b>999</b> 步</div><div class="stat"><b>0</b> tokens</div><div class="stat"><b>18535.7</b> s</div><div style="margin-top:8px"><span class="toolchip">ReadFile ×998</span><span class="toolchip">WriteFile ×998</span></div><p class="note" style="margin-top:8px">999 步 react_step,工具调用全部以文本形式嵌在每步 output 里(看 &quot;[Step N] X done&quot;)。 这是 react-over-text 在长任务里空转/重做的典型形态:步数巨大,真正落盘寥寥。
  79. </p></div>
  80. <div class="takeaway"><b>学习目标 / Takeaway</b>
  81. 对照三栏后学生应能说清:
  82. 1. 概念上,react-over-text 仍是合法的 ReAct,只是 Action 用文本承载;
  83. 2. 代码上,它落在 _compile_react 的&quot;非 nativeToolCalls&quot;分支 + ConversationLam;
  84. 3. 形式上,它引入了一个部分函数 action_parser,把&quot;解析失败&quot;这一失效模式带进了系统。
  85. 这就是&quot;为什么需要形式化&quot;的第一个实证:范式②用类型把这个失效模式提前消除。
  86. </div>
  87. <p class="rel"><b>相关对照卡:</b> <a href="tool-native-fc.html">工具调用范式②:原生 function-calling(结构化 tool_calls)</a> <a href="tool-claude-code-native.html">工具调用范式③:claude-code 原生车道(把工具调用外包给运行时)</a></p>
  88. <footer>对照卡源文件: <code>teaching/cards/01-tool-react-over-text.yml</code> ·
  89. 源码 span 由 gen.py 在生成时实时读自仓库</footer>
  90. </div></body></html>