简单来说,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
方法中,系统会:
-
工具注册检查:验证工具是否已注册 -
参数验证:使用 tool.build(args)
验证参数 -
异常捕获:捕获验证过程中的所有异常
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
方法中,系统会:
-
执行异常捕获:使用 Promise .catch()
捕获执行异常 -
错误响应生成:调用 createErrorResponse
生成标准化错误响应 -
状态更新:将工具调用状态设置为 ‘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 });
// ...
});
}
}
执行层面的取消
在工具执行过程中:
-
执行前检查:
if (signal.aborted) {
this.setStatusInternal(
reqInfo.callId,
'cancelled',
'Tool call cancelled by user.',
);
continue;
}
-
执行后检查:
.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 有这些值得学习的地方:
-
工具发现机制:三层发现机制很灵活,既有内置工具保底,又能动态扩展。 -
状态机调度:清晰的状态流转,便于调试和监控。 -
环境感知:根据运行环境自动调整行为,提升用户体验。 -
MCP 协议支持:接入标准协议,获得生态系统支持。 -
声明式工具定义:工具定义和实现分离,接口清晰。
2.5 潜在的问题
Gemini 这种设计也有一些潜在问题:
-
上下文膨胀:所有工具都提供给 LLM,可能导致 token 消耗增加。 -
工具选择不可控:完全依赖 LLM 选择,缺少人为干预手段。 -
调试困难:当工具很多时,难以追踪 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 "直接查询客户基本信息..."
# 只在需要时提供相关指令
这种设计的好处:
-
上下文精准:LLM 只看到当前需要的指令,不会被无关信息干扰。 -
缓存友好:核心系统提示词保持稳定,可以利用 LLM 的提示词缓存,提高响应速度。 -
灵活迭代:可以根据不同场景、用户群体、A/B 测试动态调整指令,不需要改动核心系统。 -
可维护性:每个工具的指令独立管理,修改一个工具的行为不会影响其他工具。
4. 小结
OpenManus 体现了优雅设计,Gemini CLI 展示简化尝试,而 Shopify Sidekick 展示了实战中的智慧。
当我们要构建生产级的 Agent 系统:
-
不要低估工具管理的复杂性 -
不要高估 LLM 的工具选择能力 -
不要忽视生产环境的残酷性
Agent 系统不是一次性工程,而是需要不断进化的。我们需要不断学习、调整、优化,才能构建出真正可靠的 AI Agent。
当我们的 Agent 要服务真实用户时,严谨的工程实践比炫酷的技术更重要。这可能不够酷,但这是通往生产环境的必经之路。
以上。