-
Notifications
You must be signed in to change notification settings - Fork 22
Expand file tree
/
Copy pathagentic-self-recursion.html
More file actions
184 lines (170 loc) · 14.8 KB
/
Copy pathagentic-self-recursion.html
File metadata and controls
184 lines (170 loc) · 14.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Agentic 函数防自递归机制 — 设计文档</title>
<style>
:root {
--bg: #0f1115; --panel: #171a21; --panel2: #1d212b; --border: #2a2f3a;
--text: #d7dbe3; --muted: #8b93a3; --accent: #6ea8ff; --accent2: #7dd1a8;
--warn: #f0b86e; --danger: #f08c8c; --code-bg: #11141a;
}
* { box-sizing: border-box; }
body {
margin: 0; background: var(--bg); color: var(--text);
font: 15px/1.7 -apple-system, "SF Pro Text", "PingFang SC", "Microsoft YaHei", sans-serif;
}
.wrap { max-width: 960px; margin: 0 auto; padding: 40px 28px 120px; }
h1 { font-size: 28px; font-weight: 700; margin: 0 0 6px; letter-spacing: -0.3px; }
h2 {
font-size: 21px; margin: 44px 0 14px; padding-bottom: 8px;
border-bottom: 1px solid var(--border); color: #fff;
}
h3 { font-size: 17px; margin: 26px 0 10px; color: var(--accent2); }
p { margin: 10px 0; }
.lead {
background: var(--panel); border: 1px solid var(--border); border-left: 3px solid var(--accent);
border-radius: 8px; padding: 14px 18px; margin: 18px 0 8px; color: var(--muted); font-size: 14px;
}
.lead code { color: var(--accent2); }
code {
background: var(--code-bg); border: 1px solid var(--border); border-radius: 4px;
padding: 1px 6px; font: 13px/1.5 "SF Mono", "JetBrains Mono", Consolas, monospace;
color: #e6c07b;
}
pre {
background: var(--code-bg); border: 1px solid var(--border); border-radius: 8px;
padding: 14px 16px; overflow-x: auto; margin: 14px 0;
}
pre code { background: none; border: none; padding: 0; color: #c8d0dc; font-size: 12.5px; }
table { border-collapse: collapse; width: 100%; margin: 16px 0; font-size: 13.5px; }
th, td { border: 1px solid var(--border); padding: 8px 12px; text-align: left; vertical-align: top; }
th { background: var(--panel2); color: #fff; font-weight: 600; }
td code { font-size: 12px; white-space: nowrap; }
ol, ul { padding-left: 22px; }
li { margin: 6px 0; }
strong { color: #fff; }
.tag { display: inline-block; font-size: 11px; padding: 2px 8px; border-radius: 20px; font-weight: 600; vertical-align: middle; }
.tag-main { background: rgba(110,168,255,.15); color: var(--accent); border: 1px solid rgba(110,168,255,.4); }
.tag-net { background: rgba(240,184,110,.15); color: var(--warn); border: 1px solid rgba(240,184,110,.4); }
.tag-del { background: rgba(240,140,140,.12); color: var(--danger); border: 1px solid rgba(240,140,140,.35); }
.fl { color: var(--accent); font-family: "SF Mono", monospace; font-size: 12px; }
.commit { color: var(--accent2); font-family: monospace; }
hr { border: none; border-top: 1px solid var(--border); margin: 36px 0; }
.toc { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 14px 20px; margin: 20px 0 30px; }
.toc a { color: var(--accent); text-decoration: none; margin-right: 18px; font-size: 13.5px; white-space: nowrap; }
.toc a:hover { text-decoration: underline; }
.note { color: var(--muted); font-size: 13px; }
</style>
</head>
<body>
<div class="wrap">
<h1>Agentic 函数防自递归机制</h1>
<div class="lead">
现状:从「deny 屏蔽工具」改成「<strong style="color:var(--accent)">处境引导</strong> + <strong style="color:var(--warn)">递归深度上限兜底</strong>」(commit <span class="commit">1f6f5fce</span>)。<br>
本文档基于真实代码逐条对应 file:line,可照着核对。<br>
相关代码:<code>function.py</code> · <code>runtime.py</code> · 测试 <code>test_self_recursion_guard.py</code>(8 用例)
</div>
<div class="toc">
<a href="#s1">1 · 问题</a>
<a href="#s2">2 · 设计理念</a>
<a href="#s3">3 · 三机制协同</a>
<a href="#s4">4 · 代码位置表</a>
<a href="#s5">5 · 行为契约</a>
<a href="#s6">6 · 新旧对比</a>
<a href="#s7">7 · 已知局限</a>
</div>
<h2 id="s1">1. 问题:agentic 函数为什么会自递归</h2>
<p>一个 agentic 函数(如 <code>wiki_agent</code>)的函数体里跑一个内层 agent loop——通过 <code>runtime.exec(content=[task])</code> 驱动内层 LLM。两个诱因叠加:</p>
<ol>
<li><strong>默认 toolset = full,含函数自己。</strong> 裸 <code>runtime.exec(content=...)</code> 不传 <code>tools=</code>/<code>toolset=</code>,解析成 <code>DEFAULT_TOOLSET = "full"</code>(<span class="fl">runtime.py:1467</span>),而 <code>full</code> 工具集列出了所有 harness 入口本身(<code>wiki_agent</code>/<code>research_agent</code>/<code>gui_agent</code>…)。<strong>内层模型的工具列表里有它正在执行的那个函数。</strong></li>
<li><strong>模型看到 docstring 匹配任务,误以为该调。</strong> 模型看到 <code>wiki_agent</code> 的描述("Maintain a wiki vault — route to ingest…")正好匹配当前任务 → 调自己 → 进去又是裸 exec、又看到自己 → 无限递归。</li>
</ol>
<p class="note">实战根因:7 层嵌套实例(<code>4d76→0c07→0964→c6f9→f1c9→4379→8746→100c</code>),记录于 <code>TODO-doc-code-gaps.md §1</code>。</p>
<h2 id="s2">2. 设计理念:为什么用「引导」而非「deny」</h2>
<p><strong>让模型理解自己的处境、自主判断不调,而不是强行从工具列表里屏蔽掉它自己。</strong></p>
<p>旧 deny 方案(wrapper 把函数自己名字推进 <code>_current_tool_policy["deny"]</code>,使内层模型看不到自己)的问题:</p>
<ul>
<li>模型学不会处境判断——它不知道「我正在 X 内部」,只是「X 不在工具列表里」。换个上下文照样犯。</li>
<li>违背理念——框架替模型做了决定,而不是给模型足够信息让它自己做对的决定。这是用户明确要求的方向:<strong>模型该知道自己在哪、自己判断不调。</strong></li>
</ul>
<p>新方案把「不调自己」变成模型能理解的一条处境信息(你正在 X 体内,调 X = 无限递归),模型据此自主不调;同时保留一个与模型判断无关的<strong>深度上限</strong>作为止损兜底。</p>
<h2 id="s3">3. 三个机制怎么协同</h2>
<h3><span class="tag tag-main">主</span> 处境提示 —— 防「发生」</h3>
<p><code>_situational_prefix(fn_name, fn_doc)</code>(<span class="fl">runtime.py:321-341</span>)生成一段英文处境提示:</p>
<pre><code>[Execution context] You are currently running INSIDE the agentic function `{fn_name}`.
The tool list may include `{fn_name}` itself — do NOT call it. Calling `{fn_name}`
re-enters where you are now and causes infinite recursion. Use lower-level tools
(search / read-write files / run code) to do the work directly.</code></pre>
<p><code>fn_doc</code> 非空时,docstring 被<strong>降级置后</strong>(<span class="fl">runtime.py:339-340</span>)——诱因(docstring 描述)不再盖过警告。</p>
<p><strong>注入到哪:user turn 开头的 text block,不进 system 前缀。</strong></p>
<ul>
<li>DAG 路径:<span class="fl">runtime.py:578-587</span> 构造 <code>frame_prefix_blocks</code>,<span class="fl">runtime.py:597</span> 拼在当前轮 <code>content</code> 之前。</li>
<li>standalone 回退(无 store):<span class="fl">runtime.py:1518-1532</span>,从 <code>_recursion_depth</code> 取最深的函数名,同样拼在 <code>content</code> 之前。</li>
<li>system 前缀单独组装(<span class="fl">runtime.py:1535-1539</span>),<strong>处境提示不进 system</strong>。</li>
</ul>
<p><strong>为什么放 user turn、不放 system:</strong> 决策6 要求项目共用一个统一恒定的 system prompt 以最大化 KV 缓存命中——前缀一变,长上下文后全不命中、成本爆。处境提示是逐函数/逐调用点变化的,放进 system 会破坏前缀恒定。放 user turn 开头既能让模型看到,又不碰 system 前缀。</p>
<h3><span class="tag tag-del">删</span> deny —— 工具列表含函数自己,靠引导不靠屏蔽</h3>
<p>wrapper 不再把函数自己名字推进 <code>_current_tool_policy["deny"]</code>。内层模型的工具列表里<strong>仍然能看到它自己</strong>,靠处境提示让模型自主不调。</p>
<p><code>_current_tool_policy</code> 的<strong>其它用途保留未动</strong>:<code>source</code>/<code>allow</code>/<code>toolset</code>/unattended deny(<span class="fl">runtime.py:1451-1483</span>)。删的只是「把函数自己名字注入 deny」这一处。</p>
<h3><span class="tag tag-net">兜底</span> 深度上限 —— 止损安全网</h3>
<ul>
<li><code>_MAX_AGENTIC_RECURSION_DEPTH = 5</code>(<span class="fl">function.py:48</span>)。</li>
<li><code>_recursion_depth</code> 是 <code>ContextVar[dict]</code>(<span class="fl">function.py:49-51</span>),存 <strong>per-function-name</strong> 的当前嵌套深度 <code>{name: depth}</code>。</li>
<li>进入 wrapper:取本函数名,读当前深度,<strong>超限抛 <code>RecursionError</code></strong>,否则 +1 写回。sync <span class="fl">function.py:964-976</span>,async <span class="fl">function.py:852-864</span>。</li>
<li>抛错条件:<code>_cur_depth >= 5</code>(已在第 5 层、要进第 6 层时抛)。</li>
<li><code>finally</code> 复位:<code>_recursion_depth.reset(token)</code>(sync <span class="fl">function.py:989</span>,async <span class="fl">function.py:877</span>)——return/异常都复位。</li>
</ul>
<p><strong>正常调用永不触及上限</strong>:处境提示先拦住「发生」,深度计数只在模型无视引导、连续 re-enter 同名函数 5 层后才触发。</p>
<p class="note">三者定位:处境提示 = 防发生 · 删 deny = 配套(工具可见,引导才有对象) · 深度上限 = 止损安全网。</p>
<h2 id="s4">4. 关键代码位置表</h2>
<table>
<tr><th>机制</th><th>代码</th><th>file:line</th></tr>
<tr><td>深度上限常量</td><td><code>_MAX_AGENTIC_RECURSION_DEPTH = 5</code></td><td><code>function.py:48</code></td></tr>
<tr><td>深度计数 contextvar</td><td><code>_recursion_depth</code></td><td><code>function.py:49-51</code></td></tr>
<tr><td>sync:本函数名</td><td><code>getattr(self,"tool_name",None) or fn.__name__</code></td><td><code>function.py:964</code></td></tr>
<tr><td>sync:超限抛错</td><td><code>if cur >= MAX: raise RecursionError</code></td><td><code>function.py:967-972</code></td></tr>
<tr><td>sync:+1 写回</td><td><code>set({**prev, name: cur+1})</code></td><td><code>function.py:973-976</code></td></tr>
<tr><td>sync:finally 复位</td><td><code>reset(token)</code></td><td><code>function.py:989</code></td></tr>
<tr><td>async:本函数名 / 抛错 / +1 / 复位</td><td>同上</td><td><code>function.py:852 / 855-860 / 861-864 / 877</code></td></tr>
<tr><td>处境提示文案</td><td><code>_situational_prefix(fn_name, fn_doc)</code></td><td><code>runtime.py:321-341</code></td></tr>
<tr><td>注入(DAG 路径)</td><td><code>frame_prefix_blocks → _build_pi_context</code></td><td><code>runtime.py:578-587, 597</code></td></tr>
<tr><td>注入(standalone 回退)</td><td>取最深名 → 拼 content 前</td><td><code>runtime.py:1518-1532</code></td></tr>
<tr><td>system 前缀(不含提示)</td><td><code>self.system + _skills_block()</code></td><td><code>runtime.py:1535-1539</code></td></tr>
<tr><td><code>_current_tool_policy</code> 其它用途</td><td>deny/source/allow/toolset 解析</td><td><code>runtime.py:1451-1483</code></td></tr>
</table>
<h2 id="s5">5. 行为契约(从测试提炼)</h2>
<p class="note">来自 <code>tests/unit/programs/test_self_recursion_guard.py</code> · 8 passed</p>
<table>
<tr><th>#</th><th>契约</th><th>测试</th></tr>
<tr><td>1</td><td>处境提示含函数名、"do NOT call it"、"recursion",docstring 降级到末尾</td><td><code>test_situational_prefix_warns_against_self_call</code></td></tr>
<tr><td>2</td><td>空 docstring 时不追加 "This function's job",提示仍含函数名</td><td><code>test_situational_prefix_handles_empty_doc</code></td></tr>
<tr><td>3</td><td>函数自己的名字<strong>不再</strong>进 deny(self-deny 删干净)</td><td><code>test_self_name_NOT_denied_during_call</code></td></tr>
<tr><td>4</td><td>正常调用一层时本函数名深度 = 1</td><td><code>test_depth_increments_during_call</code></td></tr>
<tr><td>5</td><td>无脑自调超限抛 <code>RecursionError</code>,进入函数体次数恰为上限值(到上限止住)</td><td><code>test_depth_backstop_raises_past_limit</code></td></tr>
<tr><td>6</td><td>A→B 不同名独立计数,per-name 不误伤</td><td><code>test_distinct_subcalls_not_collateral_damage</code></td></tr>
<tr><td>7</td><td>return 后深度复位</td><td><code>test_depth_restored_after_return</code></td></tr>
<tr><td>8</td><td>抛异常后深度也复位</td><td><code>test_depth_restored_after_exception</code></td></tr>
</table>
<h2 id="s6">6. 与旧 deny 方案的对比</h2>
<table>
<tr><th>维度</th><th>旧:deny 屏蔽工具</th><th>新:处境引导 + 深度上限</th></tr>
<tr><td>怎么做</td><td>函数自己名字推进 deny,内层模型看不到自己</td><td>工具列表含自己;user turn 注入处境提示让模型自主不调;超 5 层抛错兜底</td></tr>
<tr><td>模型认知</td><td>不知道「我在 X 内部」,只是 X 不在列表</td><td>明确知道处境(你在 X 体内、调 X = 递归)</td></tr>
<tr><td>system 前缀缓存</td><td>不动 system,但「替模型决定」</td><td>提示放 user turn、不进 system,前缀仍恒定(符合决策6)</td></tr>
<tr><td>失控止损</td><td>靠屏蔽间接挡(屏蔽失效就无底)</td><td>显式深度上限 5 层硬止损</td></tr>
<tr><td style="color:var(--accent2)">优</td><td>直接、无需模型配合</td><td>模型学会处境判断;符合理念;兜底确定性强</td></tr>
<tr><td style="color:var(--danger)">劣</td><td>模型学不会处境判断;违背理念;屏蔽失效就裸奔</td><td>纯引导对弱模型不 100% 可靠(故有深度上限兜底)</td></tr>
</table>
<h2 id="s7">7. 已知局限</h2>
<ol>
<li><strong>纯引导对弱模型 / 长上下文不 100% 可靠。</strong> 处境提示是让模型自主判断,弱模型或上下文过长稀释提示时可能仍调自己——所以保留深度上限作为确定性兜底。</li>
<li><strong>跨函数环(A→B→A 交替)第一版未覆盖。</strong> 深度上限按<strong>同名</strong>计数,只挡直接自递归(A→A→A…)。A→B→A→B 交替环里任一名都不到上限。整条调用链识别是增强项,未做。</li>
<li><strong>旧 deny 实现其实也只挡直接自递归、不挡跨函数环。</strong> 旧 deny 把「当前函数自己」推进 deny,B 仍可被调、B 里再调 A 也不在 B 的 deny 里。所以新方案在「跨函数环」这点上<strong>不是回退</strong>——两版都只防直接自递归,跨链识别是共同待办增强。</li>
</ol>
<hr>
<p class="note">关联文档:<code>dag/overview.md</code> 决策6(统一 system 前缀约束) · <code>TODO-doc-code-gaps.md §1</code>(7 层嵌套根因)</p>
</div>
</body>
</html>