标签归档:AIAgent

AI Agent 架构常用的 4 种设计模式

AI Agent 持续火爆,不仅仅是产品上,在融资市场也同样火爆,各种产品都在往上靠。但对于 AI Agent 该如何架构,有人关注,但少有人刻意去了解和分析。一些常见的问题有:如单个 Agent 搞不定复杂任务,多个 Agent 又容易失控,成本高,在不同的场景应该使用什么样的架构等等。

这篇文章我会尝试分享一下 AI Agent 的 4 个常用设计模式。

1. 什么是 Agent 设计模式

设计模式这个概念最早来自建筑行业,后来被软件工程借鉴过来。Christopher Alexander 在《建筑的永恒之道》里说,每个模式都是一个三元组:在特定的上下文中,解决特定的问题,采用特定的方案。

放到 AI Agent 领域,设计模式就是构建智能体系统的常见架构方法。每种模式都提供了一个组织系统组件、集成模型、编排单个或多个 Agent 来完成工作流的框架。

为什么需要设计模式?因为 Agent 系统的复杂性在于它需要自主决策、动态规划、处理不确定性。我们需要有特定场景下的特定解决方案,不过于复杂,也不过于简单,刚刚好。

选择设计模式前,需要考虑几个关键因素:任务复杂度、响应时间要求、成本预算、是否需要人工参与。想清楚这些,才能选对模式。

2. 单 Agent 模式

2.1 模式定义

单 Agent 模式是最基础的设计模式。整个系统只有一个 Agent,通过一个 AI 模型、一组预定义的工具、一个精心设计的系统提示词来完成任务。

这也是我们实际工作中常用的设计模式。

Agent 依赖模型的推理能力来理解用户请求、规划执行步骤、选择合适的工具。

这个模式的架构很简单:

用户输入 → Agent(模型+工具+提示词) → 输出结果

所有的决策和执行都在一个 Agent 内完成。

2.2 解决的问题

单 Agent 模式主要解决的是需要多步骤处理但逻辑相对清晰的任务。比如:

  • 需要调用多个 API 获取信息然后综合
  • 需要访问数据库查询后给出答案
  • 需要执行一系列操作来完成用户请求

这些任务用传统的非 Agent 系统也能做,但整个逻辑非常固化,都是规则,而使用了 Agent 后,它能动态决策,自行做工具调用。

2.3 核心组件

AI 模型:这是 Agent 的大脑,负责理解、推理和决策。模型的能力直接决定了 Agent 的上限。选择模型时要平衡能力和成本,不是所有任务都需要用最强的模型。

工具集:Agent 能调用的外部功能,比如搜索引擎、数据库、API、计算器等。工具定义要清晰,包括什么时候用、怎么用、预期结果是什么。工具太多会增加选择难度,太少又限制能力。

系统提示词:定义 Agent 的角色、任务、行为规范。好的提示词能较大幅提升 Agent 的表现。要明确告诉 Agent 它是谁、要做什么、有哪些限制、如何处理异常情况。

记忆系统:虽然不是必需的,但记忆系统能让 Agent 保持上下文,避免重复操作。可以是简单的对话历史,也可以是复杂的向量数据库。

2.4 工作流程

  1. 接收请求:Agent 接收用户的输入,可能是文本、语音或其他格式
  2. 理解意图:通过模型分析用户想要什么,需要哪些信息
  3. 制定计划:决定需要执行哪些步骤,调用哪些工具
  4. 执行操作:按计划调用工具,获取必要信息
  5. 综合结果:把各种信息整合成最终答案
  6. 返回响应:将结果返回给用户

整个过程是线性的,但 Agent 可以根据中间结果调整计划。

2.5 应用场景

客服助手:处理常见的客户询问,比如查订单、改地址、退换货。Agent 可以访问订单系统、物流系统、用户数据库,一站式解决客户问题。

研究助手:帮助用户收集和总结信息。比如搜索特定主题的最新进展,整理成报告。Agent 可以调用搜索 API、访问学术数据库、生成摘要。

个人助理:管理日程、发邮件、设置提醒。Agent 可以访问日历、邮箱、任务管理工具,帮用户处理日常事务。

2.6 优势与局限

优势:

  • 架构简单,容易实现和维护
  • 成本可控,只需要调用一个模型
  • 响应速度快,没有多 Agent 协调的开销
  • 调试方便,所有逻辑在一个地方

局限:

  • 处理复杂任务能力有限
  • 工具太多时容易混乱
  • 单点故障,Agent 出问题整个系统就挂了
  • 难以并行处理多个子任务

2.7 实施建议

从简单开始:先实现核心功能,确保基本流程跑通,再逐步添加工具和能力。

工具要精不要多:与其给 Agent 20 个工具,不如精选 5-8 个最常用的。每个工具的使用场景要明确。

提示词要迭代优化:没有一次就完美的提示词。要根据实际使用情况不断调整,特别是边界情况的处理。

加入失败处理:工具调用可能失败,模型推理可能出错。要有明确的错误处理机制,比如重试、降级、转人工。

监控关键指标:响应时间、成功率、工具调用次数、token 消耗等。这些数据是优化的基础。

3. ReAct 模式

3.1 模式定义

ReAct(Reasoning and Acting)模式是一种让 Agent 交替进行推理和行动的设计模式。不同于简单的输入输出,ReAct 模式让 Agent 在一个循环中不断地思考、行动、观察,直到找到问题的答案。

这个模式的核心思想是把 Agent 的思维过程显式化。每一步都要说明在想什么、要做什么、观察到什么,形成一个完整的推理链条。这不仅提高了结果的可靠性,也让整个过程变得可解释。

3.2 解决的问题

ReAct 模式解决的是那些需要多步探索和动态调整策略的复杂问题:

  • 答案不是显而易见的,需要逐步收集信息
  • 初始计划可能不完善,需要根据中间结果调整
  • 需要试错和迭代才能找到最优解
  • 推理过程和结果同样重要,需要可解释性

传统的一次性推理经常不够用,需要 Agent 能够根据新信息不断调整自己的理解和策略。

3.2 核心机制

Thought(思考):Agent 分析当前状况,推理下一步该做什么。这包括理解已有信息、识别缺失信息、评估可能的行动方案。思考过程要明确表达出来,比如「我需要知道 X 才能回答 Y」。

Action(行动):基于思考结果,Agent 决定采取什么行动。通常是调用某个工具获取信息,也可能是进行计算或转换。行动要具体,包括使用什么工具、传入什么参数。

Observation(观察):Agent 接收行动的结果,理解新获得的信息。观察不是简单的记录,而要分析这些信息对解决问题有什么帮助,是否需要调整策略。

这三个步骤形成一个循环,不断重复直到找到满意的答案或达到终止条件。

3.3 工作流程

用户输入问题
↓
初始思考:理解问题,确定需要什么信息
↓
循环开始:
  → 思考:基于当前信息,决定下一步
  → 行动:执行决定的操作
  → 观察:分析操作结果
  → 判断:是否已经可以回答问题?
     ├─ 否:继续循环
     └─ 是:退出循环
↓
综合所有信息,生成最终答案
↓
返回给用户

每个循环都在积累信息,逐步接近答案。关键是 Agent 要能判断什么时候信息足够了。

3.4 典型应用场景

复杂问题求解:比如数学应用题,需要分步骤求解。Agent 先理解问题,识别已知和未知,然后逐步计算中间结果,最后得出答案。每一步都要验证是否合理。

信息检索与验证:用户问一个需要多方印证的问题。Agent 从不同来源收集信息,交叉验证,排除矛盾,最终给出可靠的答案。比如”某个历史事件的真实经过”。

调试和故障排查:系统出问题了,Agent 需要逐步检查各个组件,收集日志,测试假设,最终定位问题原因。这个过程充满了试错和调整。

研究和分析:对某个主题进行深入研究。Agent 先了解背景,然后深入特定方面,发现新的线索后调整研究方向,最终形成完整的分析报告。

3.5 实现要点

推理链的质量:ReAct 模式的效果很大程度上取决于模型的推理能力。要选择推理能力强的模型,并通过提示词引导它进行结构化思考。

终止条件设计:必须有明确的终止条件,否则可能陷入无限循环。常见的终止条件包括:找到满意答案、达到最大迭代次数、遇到无法处理的错误、用户主动终止。可以参考我上一篇文章《AI Agent 核心策略:如何判断 Agent 应该停止》

上下文管理:随着循环次数增加,上下文会越来越长。需要策略性地管理上下文,比如总结之前的发现、删除无关信息、保留关键结论。

错误恢复:某一步出错不应该导致整个流程失败。要有恢复机制,比如重试、换一种方法、跳过这一步等。

3.6 优势与挑战

优势:

  • 可解释性强,每一步推理都有记录
  • 灵活性高,可以动态调整策略
  • 准确性好,通过多步验证减少错误
  • 适应性强,能处理预料之外的情况

挑战:

  • 延迟较高,多次循环导致响应时间长
  • 成本增加,每次循环都要调用模型
  • 可能陷入循环,在某些问题上来回打转
  • 对模型能力要求高,弱模型效果差

3.7 优化策略

设置合理的最大循环次数:根据任务类型和复杂度,设置合适的上限。简单任务 3-5 次,复杂任务 10-15 次。

缓存中间结果:相同的查询不要重复执行,工具调用的结果要缓存起来。

并行化某些操作:如果多个信息获取操作互不依赖,可以并行执行,减少总体时间。

使用更轻量的模型进行初步筛选:不是每个思考步骤都需要最强的模型,可以用小模型做初筛,大模型做关键决策。

提供思考模板:通过提示词工程,给 Agent 提供思考的框架,提高推理效率。

4. 多 Agent 协作模式

4.1 模式定义

多 Agent 协作模式是让多个专门化的 Agent 共同完成一个复杂任务。每个 Agent 负责自己擅长的领域,通过协调器(Coordinator)或预定义的工作流来协同工作。

这个模式的核心理念是”专业分工”。就像一个团队,每个成员都有自己的专长,通过协作可以完成单个人无法完成的任务。协调可以是中心化的(有一个协调器),也可以是去中心化的(Agent 之间直接通信)。

4.2 解决的问题

多 Agent 协作模式解决的是单个 Agent 难以处理的复杂问题:

  • 任务涉及多个专业领域,单个 Agent 难以精通所有领域
  • 需要并行处理多个子任务以提高效率
  • 任务太复杂,单个提示词难以覆盖所有情况
  • 需要不同视角的交叉验证来提高可靠性

当你发现单个 Agent 的提示词越写越长、工具越加越多、错误率开始上升时,就该考虑多 Agent 协作了。

4.3 架构类型

顺序协作:Agent 按照预定顺序依次工作,前一个的输出是后一个的输入。像流水线一样,每个 Agent 完成特定的加工步骤。适合步骤明确、顺序固定的任务。

并行协作:多个 Agent 同时工作,各自处理任务的不同方面,最后汇总结果。像团队分工一样,每个人负责一部分,最后整合。适合可以分解的独立子任务。

层级协作:Agent 组织成树状结构,上层 Agent 负责任务分解和结果汇总,下层 Agent 负责具体执行。像公司组织架构,有管理层和执行层。适合需要多级分解的复杂任务。

网状协作:Agent 之间可以自由通信,没有固定的上下级关系。像专家会诊,大家平等讨论,共同决策。适合需要充分讨论和创意的任务。

4.4 核心组件

专业 Agent:每个 Agent 专注于特定领域或功能。比如数据分析 Agent、文案撰写 Agent、代码生成 Agent 等。专业化让每个 Agent 的提示词更精简、更有效。

协调器 Agent:负责任务分解、Agent 调度、结果汇总。协调器需要理解整体任务,知道每个 Agent 的能力,能做出合理的分配决策。

通信机制:Agent 之间如何传递信息。可以是直接传递(点对点),也可以通过共享内存(如消息队列、数据库)。通信协议要明确,包括数据格式、错误处理等。一般是 json 格式。

上下文管理:如何在 Agent 之间共享和传递上下文。不是所有信息都需要传给所有 Agent,要有选择地传递相关信息,避免信息过载。

4.5 典型场景

内容创作流水线:

  • 研究 Agent:收集资料、查证事实
  • 写作 Agent:撰写初稿
  • 编辑 Agent:优化文笔、检查逻辑
  • 审核 Agent:确保符合规范、没有敏感内容

每个 Agent 专注自己的环节,整体产出高质量内容。

客户服务系统:

  • 分类 Agent:理解客户问题类型
  • 查询 Agent:从数据库获取相关信息
  • 解决方案 Agent:生成解决方案
  • 回复 Agent:组织友好的回复话术

根据问题类型,协调器可能跳过某些 Agent 或调整流程。

代码开发助手:

  • 需求分析 Agent:理解用户需求
  • 架构设计 Agent:设计系统架构
  • 代码生成 Agent:编写具体代码
  • 测试 Agent:生成测试用例并执行
  • 文档 Agent:生成代码文档

可以迭代工作,测试发现问题后返回给代码生成 Agent 修改。

数据分析系统:

  • 数据收集 Agent:从各个源获取数据
  • 清洗 Agent:处理缺失值、异常值
  • 分析 Agent:统计分析、模式识别
  • 可视化 Agent:生成图表
  • 报告 Agent:撰写分析报告

可以并行处理多个数据源,提高效率。

4.6 协调策略

中心化协调:所有决策都由协调器做出。优点是逻辑清晰、易于控制;缺点是协调器可能成为瓶颈。

分布式协调:Agent 之间直接协商。优点是灵活、无单点故障;缺点是可能出现冲突、难以调试。

混合协调:结合两者优点,重要决策由协调器做,细节由 Agent 之间协商。

动态协调:根据任务特点动态选择协调策略。简单任务用顺序协作,复杂任务用层级协作。

4.7 实施要点

明确分工:每个 Agent 的职责要清晰,避免重叠和空白。写清楚每个 Agent 负责什么、不负责什么。

接口标准化:Agent 之间的接口要标准化,包括输入输出格式、错误码、超时处理等。

错误隔离:一个 Agent 出错不应该导致整个系统崩溃。要有错误隔离和恢复机制。

性能优化:识别瓶颈 Agent,考虑并行化、缓存、负载均衡等优化手段。

版本管理:不同 Agent 可能有不同的更新频率,要有版本管理机制,确保兼容性。

4.8 优势与挑战

优势:

  • 可扩展性好,可以随时添加新的专业 Agent
  • 复用性高,Agent 可以在不同任务中复用
  • 维护性好,每个 Agent 独立维护,互不影响
  • 可靠性高,通过冗余和交叉验证提高准确性

挑战:

  • 协调开销大,Agent 之间的通信和协调需要额外成本
  • 调试困难,问题可能出现在任何一个 Agent 或它们的交互中
  • 延迟增加,多个 Agent 串行或协调都会增加总体时间
  • 成本上升,每个 Agent 都需要模型调用,成本成倍增加

5. 人机协同模式

5.1 模式定义

人机协同模式是在 Agent 工作流程中嵌入人工干预点的设计模式。Agent 在关键决策点暂停执行,等待人类审核、提供额外信息或做出决策,然后继续执行。这不是简单的人工兜底,而是人类智慧和 AI 能力的有机结合。

这个模式承认了一个现实:当前的 AI 还不能完全自主地处理所有情况,特别是涉及主观判断、伦理决策、高风险操作的场景。通过合理的人机协同,可以发挥各自优势。

在 AI 编程中通用有一个手动模式和一个自动模式。

5.2 解决的问题

人机协同模式解决的是纯 AI 方案风险太高或能力不足的问题:

  • 高风险决策需要人工确认,比如大额交易、医疗诊断
  • 主观判断 AI 难以把握,比如创意评审、品牌调性
  • 异常情况超出 AI 训练范围,需要人类经验
  • 法律或合规要求必须有人参与决策
  • AI 不确定性太高,需要人工验证

这个模式的关键是找到人机协同的最佳平衡点,既不能过度依赖人工(那就失去了自动化的意义),也不能完全放手给 AI(可能造成严重后果)。

5.3 协同机制

审核点(Checkpoint):在工作流的特定位置设置审核点,Agent 必须等待人工审核才能继续。审核点的位置很关键,太多会影响效率,太少可能错过关键决策。

升级机制(Escalation):当 Agent 遇到超出能力范围的情况时,自动升级到人工处理。需要定义清楚什么情况下升级,比如置信度低于阈值、遇到预定义的异常情况等。

协作模式(Collaboration):人类和 Agent 共同完成任务,各自负责擅长的部分。比如 Agent 做数据分析,人类做战略决策;Agent 生成初稿,人类做最终润色。

反馈循环(Feedback Loop):人类的决策和修正会反馈给 Agent,用于改进后续的行为。这是一个持续学习的过程。

5.4 干预类型

批准型干预:Agent 完成工作后,需要人工批准才能生效。比如 Agent 起草了一份合同,法务人员审核批准后才发送。这种干预主要是把关和确认。

选择型干预:Agent 提供多个选项,由人类选择。比如 Agent 生成了三个营销方案,市场总监选择最合适的。这种干预利用人类的判断力。

修正型干预:人类可以修改 Agent 的输出。比如 Agent 写了一篇文章,编辑可以直接修改其中的内容。这种干预是精细调整。

补充型干预:人类提供 Agent 缺少的信息。比如 Agent 在处理客户投诉时,遇到特殊情况,客服人员提供额外的背景信息。这种干预是信息补充。

接管型干预:在某些情况下,人类完全接管任务。比如 Agent 判断问题太复杂,直接转给人工处理。这种干预是兜底机制。

5.5 设计原则

最小干预原则:只在必要的地方设置人工干预,尽量让 Agent 自主完成任务。过多的干预会降低效率,失去自动化的意义。

透明度原则:人类要能理解 Agent 的决策依据。Agent 应该提供决策的理由、使用的数据、考虑的因素等,让人类能做出明智的判断。

可控性原则:人类要能随时介入、修改或停止 Agent 的行为。要有紧急停止按钮、回滚机制等。

责任明确原则:明确人和 AI 各自的责任边界。特别是出现问题时,要清楚责任在谁。

用户体验原则:人机交互界面要友好,信息呈现要清晰,操作要简便。不能因为加入人工环节就让流程变得复杂。

5.6 实施要点

界面设计:人机交互界面是关键。要展示必要信息,但不能信息过载。要提供便捷的操作方式,减少人工负担。可以用可视化、摘要、高亮等技术。

通知机制:需要人工介入时,要有及时的通知机制。可以是应用内通知、邮件、短信等。要考虑优先级和紧急程度。

超时处理:人工可能不能及时响应,要有超时机制。可以是自动采用保守方案、转给其他人、暂停任务等。

权限管理:不同的人可能有不同的干预权限。要有完善的权限体系,确保只有合适的人才能做关键决策。

审计追踪:所有的人工干预都要有记录。谁在什么时间做了什么决定,依据是什么。这对于问题追溯和合规审计都很重要。

5.7 优势与挑战

优势:

  • 安全性高,关键决策有人把关
  • 灵活性好,能处理 AI 无法处理的特殊情况
  • 可信度高,用户更信任有人参与的系统
  • 持续改进,人工反馈帮助系统不断优化

挑战:

  • 效率降低,人工环节会增加处理时间
  • 成本增加,需要人力投入
  • 一致性难保证,不同人可能有不同判断
  • 扩展性受限,人工环节可能成为瓶颈

6. 选择建议

选择合适的设计模式需要综合考虑多个因素。这里提供一个决策框架:

6.1 从简单到复杂的演进路径

第一阶段:单 Agent 模式

  • 任务相对简单,领域单一
  • 团队刚开始尝试 Agent
  • 需要快速验证概念
  • 成本敏感

第二阶段:ReAct 模式

  • 任务需要多步推理
  • 结果需要可解释性
  • 有一定的复杂度但还能单 Agent 处理

第三阶段:多 Agent 协作

  • 任务跨越多个领域
  • 需要专业分工
  • 单 Agent 已经力不从心

始终考虑:人机协同

  • 涉及高风险决策
  • 需要主观判断
  • 法规要求人工参与

6.2 关键决策因素

任务复杂度:

  • 低:单 Agent
  • 中:ReAct 或简单的多 Agent
  • 高:复杂的多 Agent

响应时间要求:

  • 实时(秒级):单 Agent
  • 近实时(分钟级):ReAct 或并行多 Agent
  • 非实时(小时级):任何模式都可以

成本预算:

  • 紧张:单 Agent
  • 适中:ReAct 或简单多 Agent
  • 充足:复杂多 Agent

可靠性要求:

  • 一般:单 Agent 或 ReAct
  • 高:多 Agent 协作(通过冗余提高可靠性)
  • 关键:人机协同

团队能力:

  • 初级:从单 Agent 开始
  • 中级:可以尝试 ReAct 和简单多 Agent
  • 高级:可以驾驭任何模式

7. 总结

AI Agent 的设计模式不是银弹,每种模式都有自己的适用场景和权衡。单 Agent 模式简单直接,适合入门和简单任务。ReAct 模式增加了推理能力,适合需要探索的问题。多 Agent 协作通过分工提高能力,适合复杂的领域任务。层级规划通过递归分解处理高复杂度问题。人机协同则是当前 AI 能力限制下的必要补充。

选择模式时,要从实际需求出发,考虑任务特性、资源限制、团队能力等因素。不要过度设计,也不要畏手畏脚。从简单开始,逐步演进,在实践中找到最适合的方案。

技术最终是为业务服务的。不管用什么模式,最终目的是解决实际问题,创造价值。

以上。

AI Agent 核心策略:如何判断 Agent 应该停止

简单来讲,AI Agent 实现的的大逻辑就是一个大的循环 + 获取上下文 + 不停的 LLM 调用 + 工具的调用。

那么一个关键问题就出现了:这个循环什么时候应该停止?如果处理不当,Agent 可能会陷入无限循环,浪费计算资源,或者过早停止而无法完成任务。本文将深入探讨 AI Agent 停止策略的核心设计思路。

常用停止策略

AI Agent 停止策略无外乎以下几种情况:

1. 硬性限制

最简单粗暴的方法:

  • 最大步数限制(比如最多循环 30 次)
  • 执行时间限制(比如最多跑 5 分钟)
  • API 调用次数限制(比如最多调 100 次)
  • API 调用 Token 数限制

这种方法简单有效,但用户体验很差。经常出现任务做到一半被强制停止的情况。

2. 任务完成检测

让 LLM 判断任务是否完成:

# 每次循环后问 LLM
response = llm.ask("任务是否已经完成?")
if response == "是":
    stop()

3. 显式停止信号

给 Agent 一个专门的”停止”工具:

tools = [
    "search",
    "calculate", 
    "terminate"  # 专门用来停止
]

当 Agent 调用 terminate 工具时就停止。这个方法不错,但需要在 prompt 里教会 Agent 什么时候该调用它。

4. 循环检测

检测 Agent 是否在做重复的事:

  • 连续多次调用同一个工具
  • 动作序列出现循环模式(A→B→A→B…)
  • 输出内容高度相似

5. 错误累积

连续失败多次就放弃:

if consecutive_errors > 3:
    stop("连续失败太多次")

6. 用户中断

让用户能随时喊停。

下面我们以 OpenManus 和 Gemini CLI 的源码来看一下他们是怎么做的。

OpenManus 的停止逻辑

OpenManus 的停止机制设计得比较完整,它用了一个多层防护的思路。

核心:terminate 工具

OpenManus 给每个 Agent 都配了一个 terminate 工具:

class Terminate(BaseTool):
    name: str = "terminate"
    description = """当请求已满足或无法继续时终止交互。
    完成所有任务后,调用此工具结束工作。"""
    
    async def execute(self, status: str) -> str:
        return f"交互已完成,状态:{status}"
        
以上为示例,原始代码:
from app.tool.base import BaseTool


_TERMINATE_DESCRIPTION = """Terminate the interaction when the request is met OR if the assistant cannot proceed further with the task.
When you have finished all the tasks, call this tool to end the work."""


class Terminate(BaseTool):
    name: str = "terminate"
    description: str = _TERMINATE_DESCRIPTION
    parameters: dict = {
        "type""object",
        "properties": {
            "status": {
                "type""string",
                "description""The finish status of the interaction.",
                "enum": ["success""failure"],
            }
        },
        "required": ["status"],
    }

    async def execute(self, status: str) -> str:
        """Finish the current execution"""
        return f"The interaction has been completed with status: {status}"

OpenManus 使用的是方案 3,把「何时停止」的决策权交给了 LLM。prompt 里会明确告诉 Agent:任务完成了就调用 terminate。

状态机管理

OpenManus 用状态机来管理 Agent 的生命周期:

class AgentState(Enum):
    IDLE = "idle"
    RUNNING = "running"  
    FINISHED = "finished"
    ERROR = "error"

当检测到特殊工具(如 terminate)被调用时,会触发状态转换:

async def _handle_special_tool(self, name: str, result: Any):
    if name.lower() == "terminate":
        self.state = AgentState.FINISHED
        logger.info(" 任务完成!")

步数限制

不同类型的 Agent 有不同的步数上限:

# ToolCallAgent: 30 步
# SWEAgent: 20 步
# PlanningFlow: 可配置

while self.current_step < self.max_steps and self.state != AgentState.FINISHED:
    self.current_step += 1
    await self.step()

if self.current_step >= self.max_steps:
    results.append(f"达到最大步数限制 ({self.max_steps})")

这是一个保底机制,防止 Agent 无限运行。

卡死检测

OpenManus 还会检测 Agent 是否卡住了:

def is_stuck(self) -> bool:
    # 检查是否有重复的 assistant 消息
    # 如果最近的回复都一样,说明卡住了
    recent_messages = self.get_recent_assistant_messages()
    if len(set(recent_messages)) == 1:
        return True
    return False

Planning Agent 的结束逻辑

1. 计划完成的判断机制

PlanningFlow 的结束判断并不是简单检查所有步骤是否完成:

# 在主执行循环中
while True:
    # 获取当前需要执行的步骤
    self.current_step_index, step_info = await self._get_current_step_info()
    
    # 如果没有更多活跃步骤,则结束计划
    if self.current_step_index is None:
        result += await self._finalize_plan()
        break

2. 步骤状态检查逻辑

_get_current_step_info() 方法负责判断是否还有未完成的步骤:

# 查找第一个非完成状态的步骤
for i, step in enumerate(steps):
    if i >= len(step_statuses):
        status = PlanStepStatus.NOT_STARTED.value
    else:
        status = step_statuses[i]
    
    # 如果步骤状态为活跃状态(未开始或进行中),返回该步骤
    if status in PlanStepStatus.get_active_statuses():
        return i, step_info

# 如果没找到活跃步骤,返回 None
return None, None

其中 get_active_statuses() 返回 ["not_started", "in_progress"],意味着只有当所有步骤都是 "completed""blocked" 状态时,计划才会结束。

3. 计划结束处理

当没有更多活跃步骤时,会调用 _finalize_plan() 方法:

async def _finalize_plan(self) -> str:
    """使用 LLM 生成计划完成总结"""
    plan_text = await self._get_plan_text()
    
    # 使用 LLM 生成总结
    system_message = Message.system_message(
        "You are a planning assistant. Your task is to summarize the completed plan."
    )
    
    user_message = Message.user_message(
        f"The plan has been completed. Here is the final plan status:\n\n{plan_text}\n\nPlease provide a summary of what was accomplished and any final thoughts."
    )
    
    response = await self.llm.ask(messages=[user_message], system_msgs=[system_message])
    return f"Plan completed:\n\n{response}"

Gemini CLI 的停止逻辑

Gemini CLI 的设计思路完全不同,它用了一个更优雅但也更复杂的方案。

subagent 的停止逻辑

1. 达到最大轮次(MAX_TURNS)

if (this.runConfig.max_turns && turnCounter >= this.runConfig.max_turns) {
    this.output.terminate_reason = SubagentTerminateMode.MAX_TURNS;
    break;
}

这是最简单的保护机制,防止无限循环。

2. 执行超时(TIMEOUT)

let durationMin = (Date.now() - startTime) / (1000 * 60);
if (durationMin >= this.runConfig.max_time_minutes) {
    this.output.terminate_reason = SubagentTerminateMode.TIMEOUT;
    break;
}

注意这里检查了两次超时:

  • 在调用 LLM 之前检查一次
  • 在调用 LLM 之后又检查一次

这是因为 LLM 调用可能很耗时,要确保不会超时太多。

3. 用户中断(通过 AbortSignal)

if (abortController.signal.aborted) return;

这个检查出现在 stream 处理循环里,确保能及时响应用户的取消操作。

4. 错误异常(ERROR)

catch (error) {
    console.error('Error during subagent execution:', error);
    this.output.terminate_reason = SubagentTerminateMode.ERROR;
    throw error;
}

任何未捕获的异常都会导致停止。

5. 目标完成(GOAL)

目标完成的判断分两种情况:

情况A:没有预定输出要求

if (!this.outputConfig || Object.keys(this.outputConfig.outputs).length === 0) {
    // 没有要求特定输出,LLM 不调用工具就认为完成了
    if (functionCalls.length === 0) {
        this.output.terminate_reason = SubagentTerminateMode.GOAL;
        break;
    }
}

情况B:有预定输出要求

// 检查是否所有要求的变量都已输出
const remainingVars = Object.keys(this.outputConfig.outputs).filter(
    (key) => !(key in this.output.emitted_vars)
);

if (remainingVars.length === 0) {
    this.output.terminate_reason = SubagentTerminateMode.GOAL;
    break;
}

声明式输出系统的实现

声明式输出系统的核心是 outputConfig

// 预先声明需要什么输出
this.outputConfig = {
    outputs: {
        "summary""string",
        "recommendations""array", 
        "risk_score""number"
    }
};

// Agent 通过 self.emitvalue 工具来产生输出
// 每次调用会把值存到 this.output.emitted_vars 里
this.output.emitted_vars = {
    "summary""这是总结...",
    "recommendations": ["建议1""建议2"]
    // risk_score 还没输出
};

系统会不断检查 emitted_vars 是否包含了所有 outputs 中声明的变量。只有全部输出了才认为目标完成。

Nudge 机制

Nudge(轻推)机制代码:

if (functionCalls.length === 0) {  // LLM 停止调用工具了
    // 检查是否还有变量没输出
    const remainingVars = Object.keys(this.outputConfig.outputs).filter(
        (key) => !(key in this.output.emitted_vars)
    );
    
    if (remainingVars.length > 0) {
        // 还有变量没输出,"推"它一下
        const nudgeMessage = `You have stopped calling tools but have not emitted 
        the following required variables: ${remainingVars.join(', ')}. 
        Please use the 'self.emitvalue' tool to emit them now, 
        or continue working if necessary.`;
        
        // 把提醒作为新的用户消息发给 LLM
        currentMessages = [{
            role: 'user',
            parts: [{ text: nudgeMessage }]
        }];
        
        // 继续循环,不退出
    }
}

完整的 subagent 执行流程

开始
  ↓
while (true) {
  检查是否超时/超轮次 → 是 → 退出
    ↓ 否
  调用 LLM
    ↓
  LLM 返回工具调用?
    ├─ 是 → 执行工具 → 检查目标是否完成
    │         ├─ 是 → 退出
    │         └─ 否 → 继续循环
    │
    └─ 否(LLM 停止调用工具)
         ↓
       有预定输出要求吗?
         ├─ 没有 → 退出(认为完成)
         └─ 有 → 检查是否都输出了
                   ├─ 是 → 退出
                   └─ 否 → Nudge 提醒 → 继续循环
}

三层循环检测机制

第一层:工具调用重复检测

这是最简单直接的检测,针对 Agent 反复调用相同工具的情况。

private checkToolCallLoop(toolCall: { name: string; args: object }): boolean {
    // 把工具名和参数一起哈希,生成唯一标识
    const key = this.getToolCallKey(toolCall);
    
    if (this.lastToolCallKey === key) {
        // 和上次调用完全一样,计数+1
        this.toolCallRepetitionCount++;
    } else {
        // 不一样,重置计数
        this.lastToolCallKey = key;
        this.toolCallRepetitionCount = 1;
    }
    
    // 连续5次调用相同工具+相同参数 = 循环
    if (this.toolCallRepetitionCount >= TOOL_CALL_LOOP_THRESHOLD) {
        return true;
    }
}

触发条件:连续 5 次调用完全相同的工具(包括参数)。

这种检测很严格——必须是连续的、完全相同的调用。如果中间插入了其他工具调用,计数就会重置。

第二层:内容重复检测(”咒语”检测)

这是最复杂的部分,用来检测 LLM 输出重复内容的情况,就像在念咒语一样。

private checkContentLoop(content: string): boolean {
    // 1. 先检查是否在特殊内容块中(代码块、表格、列表等)
    const numFences = (content.match(/```/g) ?? []).length;
    const hasTable = /(^|\n)\s*(\|.*\||[|+-]{3,})/.test(content);
    // ... 检查各种格式
    
    // 在代码块中不检测循环(代码本来就可能有重复)
    if (this.inCodeBlock) {
        return false;
    }
    
    // 2. 把新内容加入历史
    this.streamContentHistory += content;
    
    // 3. 保持历史在 1000 字符以内
    this.truncateAndUpdate();
    
    // 4. 分析内容块是否重复
    return this.analyzeContentChunksForLoop();
}

核心算法是滑动窗口 + 哈希检测

private analyzeContentChunksForLoop(): boolean {
    while (this.hasMoreChunksToProcess()) {
        // 提取 50 字符的块
        const currentChunk = this.streamContentHistory.substring(
            this.lastContentIndex,
            this.lastContentIndex + CONTENT_CHUNK_SIZE  // 50
        );
        
        // 计算哈希
        const chunkHash = createHash('sha256').update(currentChunk).digest('hex');
        
        // 检查这个块是否重复出现
        if (this.isLoopDetectedForChunk(currentChunk, chunkHash)) {
            return true;
        }
        
        // 滑动窗口向前移动 1 个字符
        this.lastContentIndex++;
    }
}

判断循环的条件:

private isLoopDetectedForChunk(chunk: string, hash: string): boolean {
    const existingIndices = this.contentStats.get(hash);
    
    if (!existingIndices) {
        // 第一次见到这个块,记录位置
        this.contentStats.set(hash, [this.lastContentIndex]);
        return false;
    }
    
    // 验证内容确实相同(防止哈希碰撞)
    if (!this.isActualContentMatch(chunk, existingIndices[0])) {
        return false;
    }
    
    existingIndices.push(this.lastContentIndex);
    
    // 需要出现至少 10 次
    if (existingIndices.length < CONTENT_LOOP_THRESHOLD) {  // 10
        return false;
    }
    
    // 关键:这 10 次必须距离很近(平均距离 ≤ 75 字符)
    const recentIndices = existingIndices.slice(-CONTENT_LOOP_THRESHOLD);
    const totalDistance = recentIndices[recentIndices.length - 1] - recentIndices[0];
    const averageDistance = totalDistance / (CONTENT_LOOP_THRESHOLD - 1);
    const maxAllowedDistance = CONTENT_CHUNK_SIZE * 1.5;  // 75
    
    return averageDistance <= maxAllowedDistance;
}

触发条件:同一个 50 字符的内容块,在很短的距离内重复出现 10 次

第三层:LLM 智能检测

这是最高级的检测,用 AI 来判断 AI 是否陷入循环。

private async checkForLoopWithLLM(signal: AbortSignal) {
    // 取最近 20 轮对话
    const recentHistory = this.config
        .getGeminiClient()
        .getHistory()
        .slice(-LLM_LOOP_CHECK_HISTORY_COUNT);  // 20
    
    // 清理历史(去掉悬空的函数调用等)
    const trimmedHistory = this.trimRecentHistory(recentHistory);
    
    // 让 Gemini Flash 模型分析
    const result = await this.config.getBaseLlmClient().generateJson({
        contents: [...trimmedHistory, { role: 'user', parts: [{ text: taskPrompt }] }],
        schema: {
            type'object',
            properties: {
                reasoning: { type'string' },
                confidence: { type'number' }  // 0-1 之间
            }
        },
        model: DEFAULT_GEMINI_FLASH_MODEL,
        systemInstruction: LOOP_DETECTION_SYSTEM_PROMPT
    });
    
    if (result['confidence'] > 0.9) {
        // 高置信度认为是循环
        console.warn(result['reasoning']);
        return true;
    }
}

触发时机:

async turnStarted(signal: AbortSignal) {
    this.turnsInCurrentPrompt++;
    
    if (
        this.turnsInCurrentPrompt >= LLM_CHECK_AFTER_TURNS &&  // 至少 30 轮
        this.turnsInCurrentPrompt - this.lastCheckTurn >= this.llmCheckInterval
    ) {
        this.lastCheckTurn = this.turnsInCurrentPrompt;
        return await this.checkForLoopWithLLM(signal);
    }
}
  • 必须执行超过 30 轮才开始检查(避免误判)
  • 不是每轮都检查,有间隔(默认 3 轮)
  • 间隔会根据置信度动态调整(5-15 轮)
// 动态调整检查频率
this.llmCheckInterval = Math.round(
    MIN_LLM_CHECK_INTERVAL +  // 5
    (MAX_LLM_CHECK_INTERVAL - MIN_LLM_CHECK_INTERVAL) * (1 - result['confidence'])
    // 置信度越高,检查越频繁
);

三种循环类型

系统定义了三种循环类型:

enum LoopType {
    CONSECUTIVE_IDENTICAL_TOOL_CALLS,  // 连续相同工具调用
    CHANTING_IDENTICAL_SENTENCES,      // 重复输出相同内容
    LLM_DETECTED_LOOP                  // LLM 检测到的逻辑循环
}

每种都有不同的检测方法和触发条件。

这比较适合处理长对话场景,既能有效检测循环,又不会因为过于敏感而误判正常的迭代操作。

小结

AI Agent 的停止策略是一个容易被忽视但极其重要的技术问题。从原理上看,Agent 就是一个大循环,不断调用 LLM 和工具来完成任务,但如果没有合理的停止机制,就会出现无限循环浪费资源,或者过早停止无法完成任务的问题。常见的停止方案包括硬性限制(步数、时间、API调用次数)、任务完成检测、显式停止信号、循环检测、错误累积和用户中断等,实际应用中需要组合使用多种策略。

OpenManus 采用了相对简单直接的设计:给每个 Agent 配备 terminate 工具,让 LLM 自己决定何时停止,同时用状态机管理生命周期,配合步数限制作为保底,并确保无论如何停止都会正确清理资源。

而 Gemini CLI 的设计更加精巧,核心是声明式输出系统——预先定义需要什么输出,只有全部输出才算完成,如果 Agent 停止调用工具但还有变量未输出,系统会通过 Nudge 机制温和提醒;在循环检测上,Gemini 实现了三层防护:工具调用重复检测(连续5次相同调用)、内容重复检测(滑动窗口+哈希算法检测”咒语”现象)、以及用 LLM 分析对话历史判断是否陷入逻辑循环。

实践中的关键是不要依赖单一停止机制,要组合使用多种策略形成多层防护,给 LLM 明确的停止指引,为不同类型的停止原因提供清晰的用户反馈,并确保资源能够可靠清理。停止策略的本质是在”让 Agent 完成任务”和”防止失控”之间找到平衡点。

以上。

AI Agent 核心管理逻辑:工具的管理和调度

简单来说,AI Agent 就是一个能够自主感知环境、制定计划、采取行动来完成特定目标的系统。

从架构的角度来看,又可以分为感知模块(包括状态上下文和意图上下文,而意图上下文是一个更难一些的问题),推理模块(LLM 主控部分),记忆模块,行动模块。

今天我们要聊的是贯穿于各个模块中的工具,工具不仅仅是行动模块,在感知,记忆中都有可能用到工具。

1. OpenManus 的工具管理和调度

看了 OpenManus 的代码,整个工程中 Tool 的占比还是比较大的,这也不是一个大而全的框架。

直接看代码

1.1 BaseTool 基础类

class BaseTool(ABC):
    name: str
    description: str  
    parameters: Dict[str, Any]
    
    @abstractmethod
    async def execute(self, tool_input: Dict[str, Any]) -> ToolResult

name 用于标识,description 给 LLM 看,parameters 定义输入规范,execute 执行具体逻辑。这几个字段,已经涵盖了工具管理的核心要素。

1.2 ToolCollection 管理器

工具集合的管理通过 ToolCollection 类实现,核心是一个字典:

class ToolCollection:
    def __init__(self):
        self.tool_map: Dict[str, BaseTool] = {}

整个实现有以下三个小点:

  • O(1) 的查找性能。用工具名作为 key,直接查找,没有遍历,没有复杂的匹配逻辑。
  • 统一的执行接口。所有工具调用都通过 execute 方法,传入工具名和参数,返回统一的 ToolResult。这种一致性让上层调用变得极其简单。
  • 统一的错误处理。工具不存在、执行失败都会返回 ToolFailure,不会抛异常打断流程。这种设计让系统更健壮。
def add_tool(self, tool: BaseTool) -> None:
    if tool.name in self.tool_map:
        logger.warning(f"Tool {tool.name} already exists, overwriting")
    self.tool_map[tool.name] = tool

在添加工具时可以覆盖已有工具。

1.3 Think-Act 循环

OpenManus 实现了标准的 ReAct(Reasoning and Acting)模式,但做了很多细节优化:

async def step(self) -> None:
    await self.think()  # 思考:决定用什么工具
    await self.act()    # 行动:执行工具调用

这个实现是在 ReActAgent 中定义的,think 阶段让 LLM 分析当前状态并选择工具,act 阶段执行工具并收集结果。两个阶段分离,让整个流程可控、可调试。

1.4 工具选择

工具选择是整个系统的核心:

async def think(self) -> None:
    response = await self.llm.ask_tool(
        messages=self.memory.get_messages(),
        system_prompts=self.system_prompts,
        available_tools=self.available_tools.get_tool_schemas(),
        tool_choices=self.tool_choices
    )

它把几个关键信息都传给 LLM:

  • 历史对话(messages):让 LLM 了解上下文
  • 系统提示(system_prompts):定义 Agent 的行为规范
  • 可用工具(available_tools):告诉 LLM 有哪些工具可用
  • 选择模式(tool_choices):控制是否必须选择工具

这种设计让工具选择既灵活又可控。

1.5 批量执行和结果处理

OpenManus 支持一次选择多个工具并行执行:

async def act(self) -> None:
    if self.tool_calls:
        for tool_call in self.tool_calls:
            observation = await self.execute_tool(tool_call)
            self.memory.add_message(ToolMessage(observation, tool_call.id))

每个工具的执行结果都会记录到 memory 中,供下一轮思考使用。

1.6 特殊工具处理

OpenManus 预留了特殊工具的处理机制:

def handle_special_tool(self, tool_call: ToolCall) -> str:
    if tool_call.name == "Terminate":
        self.should_stop = True
        return "Task completed successfully."
    return f"Unknown special tool: {tool_call.name}"

Terminate 工具用于优雅终止,这种设计避免了硬编码的退出逻辑。你可以轻松添加其他特殊工具,比如 Pause(暂停)、Checkpoint(保存状态)等。

2. Gemini CLI 的工具管理和调度

Gemini CLI 把所有工具都扔给大模型,让它自己选

// 就这么简单,所有工具一股脑给 LLM
async setTools(): Promise<void> {
  const toolRegistry = this.config.getToolRegistry();
  const toolDeclarations = toolRegistry.getFunctionDeclarations();
  const tools: Tool[] = [{ functionDeclarations: toolDeclarations }];
  this.getChat().setTools(tools);
}

现在的大模型已经足够聪明,能够根据上下文选择合适的工具。与其花大力气设计复杂的工具选择策略,不如相信 AI 的判断力。但会有可能存在工具「爆炸」的情况,这个后面我们再聊。

2.1 三层工具发现机制

Gemini CLI 在工具发现方面设计了三层机制:

1. 内置核心工具

这些是精心挑选的常用工具,覆盖了大部分日常开发需求:

  • 文件操作:ls、read-file、write-file、edit
  • 代码搜索:grep、ripgrep、glob
  • 系统交互:shell
  • 网络请求:web-fetch、web-search
  • 记忆管理:memory

每个工具都经过精心设计,接口清晰,功能专一。比如 edit 工具,不仅能编辑文件,还支持预览修改。

2. 命令行工具发现

这是个很巧妙的设计。我们可以通过配置一个发现命令,让系统自动发现和注册新工具:

// 执行发现命令,解析返回的工具声明
async discoverAndRegisterToolsFromCommand(): Promise<void> {
  const command = this.config.getConfigOptions().toolDiscoveryCommand;
  if (!command) return;
  
  const result = await exec(command);
  const functions = JSON.parse(result.stdout);
  
  for (const func of functions) {
    this.registerTool(new DiscoveredTool(func));
  }
}

这意味着我们可以轻松扩展工具集,只要我们的工具能输出符合格式的 JSON 声明即可。

3. MCP 服务器集成

MCP(Model Context Protocol)是个通用的协议,Gemini CLI 对它的支持相当完善:

// 支持多种传输协议
- stdio:标准输入输出
- SSE:服务器推送事件
- HTTP:REST API
- WebSocket:双向通信

通过 MCP,我们可以接入各种专业工具服务器,比如数据库查询、API 调用、专业领域工具等。每个 MCP 服务器的工具都是隔离的,避免了命名冲突。

2.2 工具调度器

Gemini CLI 的工具调度器采用了清晰的状态机模型:

// 工具执行状态流转
validating → scheduled → awaiting_approval → executing → success/error/cancelled

在执行前先验证参数,避免无效调用:

export type ValidatingToolCall = {
  status: 'validating';
  request: ToolCallRequestInfo;
  tool: AnyDeclarativeTool;
  invocation: AnyToolInvocation;
};

根据工具风险等级,提供不同的确认策略:

// 三种确认模式
ApprovalMode.DEFAULT    // 标准确认
ApprovalMode.AUTO_EDIT  // 自动编辑确认
ApprovalMode.YOLO       // 无确认模式(勇者模式)

YOLO 模式的命名有点意思,”You Only Live Once人生苦短,活出精彩

独立的工具调用可以并行执行,提高效率:

// 并行处理多个工具调用
for (const fnCall of functionCalls) {
  // 不等待,直接调度下一个
  this.handlePendingFunctionCall(fnCall);
}

工具执行过程中的输出会实时展示,用户体验很好:

// 实时更新输出流
onUpdate?: (output: string) => void;

2.3 错误处理机制

1. 错误类型分类

系统定义了完整的错误类型枚举 ToolErrorType,包括:

通用错误:

  • INVALID_TOOL_PARAMS – 工具参数无效
  • UNKNOWN – 未知错误
  • UNHANDLED_EXCEPTION – 未处理的异常
  • TOOL_NOT_REGISTERED – 工具未注册
  • EXECUTION_FAILED – 执行失败

文件系统错误:

  • FILE_NOT_FOUND – 文件未找到
  • FILE_WRITE_FAILURE – 文件写入失败
  • PERMISSION_DENIED – 权限拒绝
  • PATH_NOT_IN_WORKSPACE – 路径不在工作空间内

Shell 特定错误:

  • SHELL_EXECUTE_ERROR – Shell 执行错误

2. 工具调用状态管理

系统定义了完整的工具调用状态类型:

export type ToolCall =
  | ValidatingToolCall      // 验证中
  | ScheduledToolCall       // 已调度
  | ErroredToolCall         // 错误
  | SuccessfulToolCall      // 成功
  | ExecutingToolCall       // 执行中
  | CancelledToolCall       // 已取消
  | WaitingToolCall;        // 等待批准

3. 错误处理流程

验证阶段错误处理

_schedule 方法中,系统会:

  1. 工具注册检查:验证工具是否已注册
  2. 参数验证:使用 tool.build(args) 验证参数
  3. 异常捕获:捕获验证过程中的所有异常
try {
  const invocationOrError = this.buildInvocation(toolInstance, args);
  if (invocationOrError instanceof Error) {
    return {
      status: 'error',
      request: reqInfo,
      response: createErrorResponse(
        reqInfo,
        invocationOrError,
        ToolErrorType.INVALID_TOOL_PARAMS,
      ),
      durationMs: 0,
    };
  }
} catch (error) {
  // 处理验证异常
}

执行阶段错误处理

attemptExecutionOfScheduledCalls 方法中,系统会:

  1. 执行异常捕获:使用 Promise .catch() 捕获执行异常
  2. 错误响应生成:调用 createErrorResponse 生成标准化错误响应
  3. 状态更新:将工具调用状态设置为 ‘error’
promise
  .then(async (toolResult: ToolResult) => {
    // 处理成功结果
  })
  .catch((error: Error) => {
    this.setStatusInternal(
      callId,
      'error',
      createErrorResponse(reqInfo, error, ToolErrorType.UNHANDLED_EXCEPTION),
    );
  });

4. 错误响应格式

createErrorResponse 函数生成标准化的错误响应:

const createErrorResponse = (
  request: ToolCallRequestInfo,
  error: Error,
  errorType: ToolErrorType | undefined,
): ToolCallResponseInfo => ({
  callId: request.callId,
  error,
  responseParts: [{
    functionResponse: {
      id: request.callId,
      name: request.name,
      response: { error: error.message },
    },
  }],
  resultDisplay: error.message,
  errorType,
  contentLength: error.message.length,
});

2.4 超时策略

1. AbortSignal 机制

系统使用 AbortSignal 实现超时和取消控制:

调度层面的取消

schedule 方法中:

schedule(
  request: ToolCallRequestInfo | ToolCallRequestInfo[],
  signal: AbortSignal,
): Promise<void> {
  if (this.isRunning() || this.isScheduling) {
    return new Promise((resolve, reject) => {
      const abortHandler = () => {
        // 从队列中移除请求
        const index = this.requestQueue.findIndex(
          (item) => item.request === request,
        );
        if (index > -1) {
          this.requestQueue.splice(index, 1);
          reject(new Error('Tool call cancelled while in queue.'));
        }
      };
      
      signal.addEventListener('abort', abortHandler, { once: true });
      // ...
    });
  }
}

执行层面的取消

在工具执行过程中:

  1. 执行前检查
if (signal.aborted) {
  this.setStatusInternal(
    reqInfo.callId,
    'cancelled',
    'Tool call cancelled by user.',
  );
  continue;
}
  1. 执行后检查
.then(async (toolResult: ToolResult) => {
  if (signal.aborted) {
    this.setStatusInternal(
      callId,
      'cancelled',
      'User cancelled tool execution.',
    );
    return;
  }
  // 处理成功结果
})

2. Shell 工具特殊处理

Shell 工具有特殊的超时和取消机制:

进程 ID 跟踪

if (invocation instanceof ShellToolInvocation) {
  const setPidCallback = (pid: number) => {
    this.toolCalls = this.toolCalls.map((tc) =>
      tc.request.callId === callId && tc.status === 'executing'
        ? { ...tc, pid }
        : tc,
    );
    this.notifyToolCallsUpdate();
  };
  promise = invocation.execute(
    signal,
    liveOutputCallback,
    shellExecutionConfig,
    setPidCallback,
  );
}

Shell 执行配置

系统通过 ShellExecutionConfig 配置 Shell 执行环境:

export interface ShellExecutionConfig {
  terminalWidth?: number;
  terminalHeight?: number;
  pager?: string;
  showColor?: boolean;
  defaultFg?: string;
  defaultBg?: string;
}

3. 输出截断机制

为防止输出过大导致内存问题,系统实现了输出截断:

if (
  typeof content === 'string' &&
  toolName === ShellTool.Name &&
  this.config.getEnableToolOutputTruncation() &&
  this.config.getTruncateToolOutputThreshold() > 0 &&
  this.config.getTruncateToolOutputLines() > 0
) {
  const truncatedResult = await truncateAndSaveToFile(
    content,
    callId,
    this.config.storage.getProjectTempDir(),
    threshold,
    lines,
  );
  content = truncatedResult.content;
  outputFile = truncatedResult.outputFile;
}

如果我们要构建自己的 Agent 系统,Gemini CLI 有这些值得学习的地方:

  1. 工具发现机制:三层发现机制很灵活,既有内置工具保底,又能动态扩展。
  2. 状态机调度:清晰的状态流转,便于调试和监控。
  3. 环境感知:根据运行环境自动调整行为,提升用户体验。
  4. MCP 协议支持:接入标准协议,获得生态系统支持。
  5. 声明式工具定义:工具定义和实现分离,接口清晰。

2.5 潜在的问题

Gemini 这种设计也有一些潜在问题:

  1. 上下文膨胀:所有工具都提供给 LLM,可能导致 token 消耗增加。
  2. 工具选择不可控:完全依赖 LLM 选择,缺少人为干预手段。
  3. 调试困难:当工具很多时,难以追踪 LLM 为什么选择某个工具。

3. Shopify Sidekick 的工具管理策略

文章地址:https://shopify.engineering/building-production-ready-agentic-systems

Shopify 团队在其文章中告诉我们一个他们踩的坑:工具数量的增长不是线性问题,而是指数级复杂度问题

他们把这个过程分成了三个阶段:

0-20 个工具:蜜月期。每个工具职责清晰,调试简单,系统行为可预测。这个阶段你会觉得”Agent 开发也不过如此嘛”。

20-50 个工具:混乱期。工具边界开始模糊,组合使用时产生意想不到的结果。这时候你开始怀疑人生:”为什么调用查询工具的同时会触发邮件发送?”

50+ 个工具:崩溃期。同一个任务有多种实现路径,系统变得无法理解。调试像在迷宫里找出口,修一个 bug 引入三个新 bug。

当工具数量超过 30 个后,整个系统变得难以维护。Shopify 把这个问题叫做”Death by a Thousand Instructions”(千条指令之死),系统提示词变成了一个充满特殊情况、相互冲突的指导和边缘案例处理的怪物。

3.1 Just-in-Time Instructions

面对工具管理的混乱,Shopify 团队找到了一个巧妙的解决方案:Just-in-Time (JIT) Instructions

这个思路其实很简单,可能的实现如下:

# 传统方式:所有指令都塞在系统提示词里
system_prompt = """
你是一个AI助手...
当用户查询客户时,使用customer_query工具...
当用户需要发邮件时,先检查权限...
如果是VIP客户,要特别注意...
处理订单时要考虑库存...
(还有500行各种规则)
"""

# JIT方式:按需提供指令
def get_tool_instructions(tool_name, context):
    if tool_name == "customer_query":
        if context.is_filtering:
            return "使用customer_tags进行标签筛选,使用customer_status进行状态筛选..."
        else:
            return "直接查询客户基本信息..."
    # 只在需要时提供相关指令

这种设计的好处:

  1. 上下文精准:LLM 只看到当前需要的指令,不会被无关信息干扰。
  2. 缓存友好:核心系统提示词保持稳定,可以利用 LLM 的提示词缓存,提高响应速度。
  3. 灵活迭代:可以根据不同场景、用户群体、A/B 测试动态调整指令,不需要改动核心系统。
  4. 可维护性:每个工具的指令独立管理,修改一个工具的行为不会影响其他工具。

4. 小结

OpenManus 体现了优雅设计,Gemini CLI 展示简化尝试,而 Shopify Sidekick 展示了实战中的智慧。

当我们要构建生产级的 Agent 系统:

  • 不要低估工具管理的复杂性
  • 不要高估 LLM 的工具选择能力
  • 不要忽视生产环境的残酷性

Agent 系统不是一次性工程,而是需要不断进化的。我们需要不断学习、调整、优化,才能构建出真正可靠的 AI Agent。

当我们的 Agent 要服务真实用户时,严谨的工程实践比炫酷的技术更重要。这可能不够酷,但这是通往生产环境的必经之路。

以上。