AgentLoop 工具调用的设计问题与解决方案

1852 字
9 分钟
AgentLoop 工具调用的设计问题与解决方案

AgentLoop 工具调用的设计问题与解决方案#


一、开发者关注的核心问题#

┌─────────────────────────────────────────────────────────┐
│ 1. 并发控制 - 工具间是否有依赖?如何执行? │
│ 2. 参数验证 - LLM 返回的参数可靠吗? │
│ 3. 中断取消 - 长任务如何优雅停止? │
│ 4. 错误处理 - 工具失败会不会崩溃整个流程? │
│ 5. 流式反馈 - 用户等待时能看到进度吗? │
│ 6. 执行拦截 - 危险操作需要确认吗? │
│ 7. 结果修改 - 工具返回能被后处理吗? │
│ 8. 终止信号 - 工具能主动告诉 loop 停吗? │
│ 9. 事件顺序 - 并行执行如何保证顺序正确? │
│ 10. 上下文管理 - 结果如何正确加入对话? │
└─────────────────────────────────────────────────────────┘

二、问题 1:并发控制#

问题场景#

LLM 同时调用多个工具:
readFile("secret.txt") → 需要读取
writeFile("secret.txt") → 需要写入
如果并行执行 → readFile 可能读到写入中的半截内容

解决方案:执行模式 + 工具声明#

// types.ts - 工具可以声明自己的执行需求
interface AgentTool {
executionMode?: "sequential" | "parallel"
// 如果这个工具需要串行,就声明 sequential
}
// agent-loop.ts - 判断逻辑
const hasSequentialToolCall = toolCalls.some(tc =>
tools.find(t => t.name === tc.name)?.executionMode === "sequential"
)
// 只要有一个工具声明 sequential,整批都串行
if (config.toolExecution === "sequential" || hasSequentialToolCall) {
return executeToolCallsSequential(...)
}

设计思路#

传统方案AgentLoop 方案
全局串行(低效)
人工编排顺序
无灵活配置
默认并行 + 工具自声明
工具自己声明需求
全局配置 + 单工具覆盖

三、问题 2:参数验证#

问题场景#

LLM 返回参数:
toolCall.arguments = { path: "test.txt", mode: 123 }
但 schema 定义:
mode: { type: "string", enum: ["read", "write"] }
如果直接传给工具 → 类型错误 / 运行时崩溃

解决方案:prepareArguments + validateToolArguments#

两层设计:

Layer 1: prepareArguments(兼容层)

  • 作用:修复 LLM 返回的”小问题”
    • 123 → “123”
    • “read” → “READ”
    • 缺少默认值 → 补上

Layer 2: validateToolArguments(校验层)

  • 作用:严格 schema 校验
    • 类型匹配、enum 检查、required 检查
为什么两层?
1. prepareArguments 可选
├─ 不写 → 直接校验(严格模式)
└─ 写了 → 先修复再校验(兼容模式)
2. 分离关注点
├─ prepareArguments:工具开发者负责
│ "我知道我的工具可能收到什么奇怪参数"
└─ validateToolArguments:Loop 负责
│ "我只接受符合 schema 的参数"

四、问题 3:中断取消#

Signal 检查点分布#

┌─────────────────────────────────────────┐
│ Check 1: prepareToolCall 结束 │
│ if (signal?.aborted) → 返回错误结果 │
│ │
│ Check 2: beforeToolCall 后 │
│ if (signal?.aborted) → 返回错误结果 │
│ │
│ Check 3: executePreparedToolCall 中 │
│ ├─ 传给 tool.execute │
│ ├─ 工具内部应该检查 signal │
│ └─ 工具应该抛错或返回 │
│ │
│ Check 4: finalizeExecutedToolCall │
│ ├─ 传给 afterToolCall │
│ ├─ hook 应该检查 signal │
│ │
│ Check 5: Sequential 每个工具后 │
│ if (signal?.aborted) break │
│ │
│ Check 6: Parallel 准备阶段 │
│ if (signal?.aborted) break │
└─────────────────────────────────────────┘

设计思路#

AbortSignal 设计原则:
1. 传递而非检查
├─ Loop 不主动中断工具
├─ Loop 只是传递 signal
└─ 工具自己决定如何响应
2. 多层检查
├─ Loop 在关键节点检查
├─ Hook 收到 signal 应该检查
└─ Tool 收到 signal 应该检查
3. 快速失败
├─ 发现 aborted → 立即返回错误结果
├─ 不等待工具完成
└─ 不尝试"优雅停止"

五、问题 4:错误处理#

错误边界#

┌─────────────────────────────────────────┐
│ Error Boundary 1: tool.execute │
│ │
│ 工具抛错 → 包装成 AgentToolResult │
│ ├─ isError = true │
│ ├─ content = "Error: xxx" │
│ └─ 流程继续,不中断 │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Error Boundary 2: prepareToolCall │
│ │
│ 参数校验失败 → 包装成错误结果 │
│ ├─ isError = true │
│ ├─ content = "Validation failed" │
│ └─ 返回 ImmediateToolCallOutcome │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Error Boundary 3: afterToolCall │
│ │
│ Hook 抛错 → 包装成错误结果 │
│ ├─ isError = true │
│ ├─ content = "Hook error: xxx" │
│ └─ 覆盖原工具结果 │
└─────────────────────────────────────────┘

设计思路#

为什么"错误也是结果"?
优势:
1. LLM 看到错误信息
├─ 可以调整策略
├─ 可以换工具
└─ 可以放弃任务
2. 错误是对话的一部分
├─ 用户能看到
├─ 日志能记录
└─ UI 能渲染
3. 流程不中断
├─ 其他工具继续执行
└─ 整体任务可能完成

六、问题 5:流式反馈#

流式更新机制#

┌─────────────────────────────────────────┐
│ Tool 实现 │
│ │
│ async execute(id, params, signal, onUpdate) {│
│ // 阶段 1 │
│ onUpdate({ details: { stage: "init" } })│
│ await init() │
│ │
│ // 阶段 2 │
│ onUpdate({ details: { stage: "process" } })│
│ for (item of items) { │
│ onUpdate({ details: { progress: i/total } })│
│ await process(item) │
│ } │
│ │
│ // 阶段 3 │
│ onUpdate({ details: { stage: "done" } })│
│ return finalResult │
│ } │
└─────────────────────────────────────────┘
↓ onUpdate 回调
┌─────────────────────────────────────────┐
│ Loop 处理 │
│ │
│ emit tool_execution_update │
│ ├─ UI 收到事件 │
│ ├─ 显示进度 │
│ └─ 用户看到实时反馈 │
│ │
│ 注意:update 事件不阻塞执行 │
└─────────────────────────────────────────┘

七、设计原则总结#

┌─────────────────────────────────────────────────────────────┐
│ AgentLoop 工具调用设计原则 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 分离关注点 │
│ ├─ Tool:只知道如何执行 │
│ ├─ Hook:知道是否执行 / 如何处理 │
│ ├─ Loop:知道何时执行 / 如何编排 │
│ └─ 各司其职 │
│ │
│ 2. 错误即结果 │
│ ├─ 错误包装成 AgentToolResult │
│ ├─ isError 标记 │
│ ├─ 流程不中断 │
│ ├─ LLM 看到错误 │
│ └─ LLM 决定下一步 │
│ │
│ 3. 传递而非控制 │
│ ├─ AbortSignal 传递给所有层级 │
│ ├─ Loop 不主动中断 │
│ ├─ Tool / Hook 自己响应 │
│ └─ 各层自主决定 │
│ │
│ 4. 替换而非合并 │
│ ├─ AfterToolCallResult 替换语义 │
│ ├─ 明确意图 │
│ ├─ 防止歧义 │
│ └─ 简化实现 │
│ │
│ 5. 工具自声明 │
│ ├─ executionMode 由工具声明 │
│ ├─ 工具知道自己是否安全并发 │
│ ├─ Loop 根据声明调整 │
│ └─ 灵活 + 安全 │
│ │
│ 6. 双重验证 │
│ ├─ prepareArguments:兼容层(可选) │
│ ├─ validateToolArguments:校验层(强制) │
│ ├─ 先修复后校验 │
│ └─ 最终符合 schema │
│ │
│ 7. 流式非阻塞 │
│ ├─ onUpdate 回调 │
│ ├─ emit 不阻塞执行 │
│ ├─ 批量 await │
│ ├─ 实时反馈 │
│ │
│ 8. 顺序分离 │
│ ├─ tool_execution_end:完成顺序(实时性) │
│ ├─ message_start/end:原始顺序(连贯性) │
│ ├─ 不同用途 │
│ ├─ 各有意义 │
│ │
│ 9. 双 Context │
│ ├─ currentContext:运行时完整对话 │
│ ├─ newMessages:本次产出 │
│ ├─ 分离关注点 │
│ ├─ 调用者灵活 │
│ │
│ 10. 拦截统一入口 │
│ ├─ beforeToolCall 集中拦截逻辑 │
│ ├─ 容易维护 │
│ ├─ 容易审计 │
│ ├─ 工具实现简单 │
│ │
└─────────────────────────────────────────────────────────────┘

一句话总结#

开发者关注的问题:
并发/参数/中断/错误/反馈/拦截/修改/终止/顺序/上下文
核心设计原则:
分离关注点 + 错误即结果 + 传递而非控制 + 替换而非合并
+ 工具自声明 + 双重验证 + 流式非阻塞 + 顺序分离 + 双Context + 拦截统一入口

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
AgentLoop 工具调用的设计问题与解决方案
https://firefly.cuteleaf.cn/posts/learn-pi/agent-loop/14-工具调用设计问题解决方案/
作者
AltumSisy
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
AltumSisy
Hello, I'm AltumSisy.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
32
分类
3
标签
25
总字数
68,347
运行时长
0
最后活动
0 天前

文章目录