function.py · runtime.py · 测试 test_self_recursion_guard.py(8 用例)
一个 agentic 函数(如 wiki_agent)的函数体里跑一个内层 agent loop——通过 runtime.exec(content=[task]) 驱动内层 LLM。两个诱因叠加:
runtime.exec(content=...) 不传 tools=/toolset=,解析成 DEFAULT_TOOLSET = "full"(runtime.py:1467),而 full 工具集列出了所有 harness 入口本身(wiki_agent/research_agent/gui_agent…)。内层模型的工具列表里有它正在执行的那个函数。wiki_agent 的描述("Maintain a wiki vault — route to ingest…")正好匹配当前任务 → 调自己 → 进去又是裸 exec、又看到自己 → 无限递归。实战根因:7 层嵌套实例(4d76→0c07→0964→c6f9→f1c9→4379→8746→100c),记录于 TODO-doc-code-gaps.md §1。
让模型理解自己的处境、自主判断不调,而不是强行从工具列表里屏蔽掉它自己。
旧 deny 方案(wrapper 把函数自己名字推进 _current_tool_policy["deny"],使内层模型看不到自己)的问题:
新方案把「不调自己」变成模型能理解的一条处境信息(你正在 X 体内,调 X = 无限递归),模型据此自主不调;同时保留一个与模型判断无关的深度上限作为止损兜底。
_situational_prefix(fn_name, fn_doc)(runtime.py:321-341)生成一段英文处境提示:
[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.
fn_doc 非空时,docstring 被降级置后(runtime.py:339-340)——诱因(docstring 描述)不再盖过警告。
注入到哪:user turn 开头的 text block,不进 system 前缀。
frame_prefix_blocks,runtime.py:597 拼在当前轮 content 之前。_recursion_depth 取最深的函数名,同样拼在 content 之前。为什么放 user turn、不放 system: 决策6 要求项目共用一个统一恒定的 system prompt 以最大化 KV 缓存命中——前缀一变,长上下文后全不命中、成本爆。处境提示是逐函数/逐调用点变化的,放进 system 会破坏前缀恒定。放 user turn 开头既能让模型看到,又不碰 system 前缀。
wrapper 不再把函数自己名字推进 _current_tool_policy["deny"]。内层模型的工具列表里仍然能看到它自己,靠处境提示让模型自主不调。
_current_tool_policy 的其它用途保留未动:source/allow/toolset/unattended deny(runtime.py:1451-1483)。删的只是「把函数自己名字注入 deny」这一处。
_MAX_AGENTIC_RECURSION_DEPTH = 5(function.py:48)。_recursion_depth 是 ContextVar[dict](function.py:49-51),存 per-function-name 的当前嵌套深度 {name: depth}。RecursionError,否则 +1 写回。sync function.py:964-976,async function.py:852-864。_cur_depth >= 5(已在第 5 层、要进第 6 层时抛)。finally 复位:_recursion_depth.reset(token)(sync function.py:989,async function.py:877)——return/异常都复位。正常调用永不触及上限:处境提示先拦住「发生」,深度计数只在模型无视引导、连续 re-enter 同名函数 5 层后才触发。
三者定位:处境提示 = 防发生 · 删 deny = 配套(工具可见,引导才有对象) · 深度上限 = 止损安全网。
| 机制 | 代码 | file:line |
|---|---|---|
| 深度上限常量 | _MAX_AGENTIC_RECURSION_DEPTH = 5 | function.py:48 |
| 深度计数 contextvar | _recursion_depth | function.py:49-51 |
| sync:本函数名 | getattr(self,"tool_name",None) or fn.__name__ | function.py:964 |
| sync:超限抛错 | if cur >= MAX: raise RecursionError | function.py:967-972 |
| sync:+1 写回 | set({**prev, name: cur+1}) | function.py:973-976 |
| sync:finally 复位 | reset(token) | function.py:989 |
| async:本函数名 / 抛错 / +1 / 复位 | 同上 | function.py:852 / 855-860 / 861-864 / 877 |
| 处境提示文案 | _situational_prefix(fn_name, fn_doc) | runtime.py:321-341 |
| 注入(DAG 路径) | frame_prefix_blocks → _build_pi_context | runtime.py:578-587, 597 |
| 注入(standalone 回退) | 取最深名 → 拼 content 前 | runtime.py:1518-1532 |
| system 前缀(不含提示) | self.system + _skills_block() | runtime.py:1535-1539 |
_current_tool_policy 其它用途 | deny/source/allow/toolset 解析 | runtime.py:1451-1483 |
来自 tests/agentic_programming/test_self_recursion_guard.py · 8 passed
| # | 契约 | 测试 |
|---|---|---|
| 1 | 处境提示含函数名、"do NOT call it"、"recursion",docstring 降级到末尾 | test_situational_prefix_warns_against_self_call |
| 2 | 空 docstring 时不追加 "This function's job",提示仍含函数名 | test_situational_prefix_handles_empty_doc |
| 3 | 函数自己的名字不再进 deny(self-deny 删干净) | test_self_name_NOT_denied_during_call |
| 4 | 正常调用一层时本函数名深度 = 1 | test_depth_increments_during_call |
| 5 | 无脑自调超限抛 RecursionError,进入函数体次数恰为上限值(到上限止住) | test_depth_backstop_raises_past_limit |
| 6 | A→B 不同名独立计数,per-name 不误伤 | test_distinct_subcalls_not_collateral_damage |
| 7 | return 后深度复位 | test_depth_restored_after_return |
| 8 | 抛异常后深度也复位 | test_depth_restored_after_exception |
| 维度 | 旧:deny 屏蔽工具 | 新:处境引导 + 深度上限 |
|---|---|---|
| 怎么做 | 函数自己名字推进 deny,内层模型看不到自己 | 工具列表含自己;user turn 注入处境提示让模型自主不调;超 5 层抛错兜底 |
| 模型认知 | 不知道「我在 X 内部」,只是 X 不在列表 | 明确知道处境(你在 X 体内、调 X = 递归) |
| system 前缀缓存 | 不动 system,但「替模型决定」 | 提示放 user turn、不进 system,前缀仍恒定(符合决策6) |
| 失控止损 | 靠屏蔽间接挡(屏蔽失效就无底) | 显式深度上限 5 层硬止损 |
| 优 | 直接、无需模型配合 | 模型学会处境判断;符合理念;兜底确定性强 |
| 劣 | 模型学不会处境判断;违背理念;屏蔽失效就裸奔 | 纯引导对弱模型不 100% 可靠(故有深度上限兜底) |
关联文档:dag/overview.md 决策6(统一 system 前缀约束) · TODO-doc-code-gaps.md §1(7 层嵌套根因)