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-工具调用设计问题解决方案/ 相关文章 智能推荐
1
AgentLoop 工具调用的本质问题与核心原则
pi 从10个具体问题抽象出5个本质问题,推导出5个核心原则:声明式设计、传递式控制、结果式错误、Hook统一拦截、双层分离
2
Loop与Agent 问题驱动的架构设计分析
pi 从表面问题出发,抽象本质问题,推导核心原则,展示技术实现的完整设计过程,理解agent-loop.ts与agent.ts的分层职责
3
Agent Loop 核心架构解析
pi 基于 agent-loop.ts 源码分析,理解双层循环架构、四大核心机制、五大问题抽象
4
Agent Loop 核心机制详解
pi 深入理解双入口设计、双层循环架构、三种消息注入机制、工具执行模式
5
AbortController 与 terminate 设计原理
pi 深入理解两种中断机制的设计原理:AbortController的预先注册模式与terminate的返回值携带模式
随机文章 随机推荐