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 要服务真实用户时,严谨的工程实践比炫酷的技术更重要。这可能不够酷,但这是通往生产环境的必经之路。

以上。

发表评论

电子邮件地址不会被公开。 必填项已用*标注


*

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>