手搓 Agent 系列 02|Tool 规模化的 3 个工程问题:成本、选择、维护

手搓 Agent 系列 02|Tool 规模化的 3 个工程问题:成本、选择、维护

在上一篇里,我们把第一个 Agent 跑起来了。它根据用户的要求决定下一步做什么,能通过一个简单的循环不断行动,直到任务完成。

但这个 Agent 还有个明显的短板:它能"想",却不一定能真正"做"。比如我让它写一篇关于 AI Agent 的公众号文章,它可以直接产出一篇出来,却不能主动搜索最近的资料、读取文件,或者调用外部服务。

要让它具备这些能力,就是 Tool要做的事。

这一篇我们继续用同一个例子演示:帮我写一篇关于 AI Agent 的公众号文章,先搜索资料,再完成写作。 我们先自己实现最小版本,再看看当 Tool 越来越多、调用越来越复杂时,工程上会遇到什么问题,业界又是怎么处理的。

代码重点展示核心逻辑。示例中的 search_web 使用模拟数据,方便直接看懂调用过程;接真实搜索服务时,只需要替换它的内部实现。不同Agent 的 Tool Calling 接口会有差异,下面用接近通用结构的代码说明运行机制。

一、Tool Calling 的完整执行链路

普通的 LLM 程序通常是:把用户的问题发给模型,再把模型生成的文本返回给用户。模型输出的内容本身不会自动变成一次真实的程序调用。

Tool Calling 改变了这一点。我们先告诉模型当前有哪些 Tool、每个 Tool 能做什么、需要什么参数。模型判断任务需要调用某个 Tool 时,会返回一条结构化的调用请求;真正执行函数的仍然是我们的程序。

整个过程可以理解为:

text 复制代码
用户提出需求
    ↓
把可用 Tool 的定义交给 LLM
    ↓
LLM 返回 Tool Call
    ↓
Agent Tool Runtime 校验并执行 Tool
    ↓
把 Tool Result 返回给 LLM
    ↓
LLM 决定下一步:继续调用 Tool,还是回答用户

比如用户要写一篇近期的 AI Agent 文章,模型可能先请求 search_web。程序执行搜索,把搜索结果交还给模型,模型再根据这些结果组织文章。如果模型还需要查其他资料,就可以继续发起下一次调用。

二、先手搓一个最小 Tool

我们只定义一个搜索函数,再用一个字典把 Tool 名称和 Python 函数对应起来。

python 复制代码
def search_web(query: str) -> dict:
    """搜索互联网资料。这里用模拟结果演示调用过程。"""
    return {
        "query": query,
        "results": [
            {
                "title": "Agent 工程实践",
                "snippet": "Agent 通常需要结合模型决策、工具执行和结果反馈。"
            },
            {
                "title": "Tool Calling 入门",
                "snippet": "模型返回结构化调用请求,由应用程序执行对应工具。"
            }
        ]
    }


# Runtime 保存真正的执行函数
tool_registry = {
    "search_web": search_web,
}

模型不能只凭一个函数名就知道该传什么参数,所以我们还需要一份给模型看的 Tool Definition。它描述工具的名称、用途和输入参数,但不包含真正的 Python 执行代码。

python 复制代码
tool_definitions = [
    {
        "name": "search_web",
        "description": "搜索互联网资料,适合查找近期信息或外部参考资料",
        "parameters": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "需要搜索的关键词或问题"
                }
            },
            "required": ["query"]
        }
    }
]

注意,这里有两份东西:tool_definitions 是交给模型看的能力说明,tool_registry 是Agent Runtime 用来找到执行函数的映射。它们通过 Tool Name 对应起来。这就是 Tool Definition 与 Tool Implementation 分离的最小例子。

当模型返回 Tool Call 后,Runtime 就可以按名称找到函数并执行。真实项目里还需要检查工具是否存在、参数是否合法,并处理异常;这里先看核心逻辑:

python 复制代码
def execute_tool(name: str, arguments: dict):
    tool = tool_registry.get(name)
    if tool is None:
        raise ValueError(f"未知的 Tool: {name}")

    return tool(**arguments)


# 模拟 LLM 返回的 Tool Call
tool_call = {
    "name": "search_web",
    "arguments": {"query": "AI Agent 工程实践"}
}

result = execute_tool(
    tool_call["name"],
    tool_call["arguments"]
)
print(result)

实际接模型 API 时,不需要自己让模型随意拼一段文本再猜它的意思,而是把 tool_definitions 通过模型 API 的 Tool Calling 参数传入,读取 API 返回的结构化 Tool Call,再交给 execute_tool 执行。具体字段名取决于所使用的模型服务。

执行完还没结束。Runtime 得把结果作为 Tool Result 回传给模型,让它基于结果继续决策。模型可能再调用一个工具,也可能直接生成最终回答。这就是 Tool Calling 的完整闭环。

三、Tool 执行失败怎么办

只要开始调用外部能力,失败就不可避免。搜索服务可能超时,API 可能暂时不可用,也可能返回"账号配额已用完"。这些错误看起来都是失败,但处理方式并不相同。

比如网络超时有可能稍后重试就成功;如果服务明确告诉我们配额已经用完,再重试通常只会浪费时间和成本。Runtime 不应该不分情况地重试所有错误。

先把执行过程包上一层异常处理:

python 复制代码
def execute_tool_safely(name: str, arguments: dict) -> dict:
    try:
        result = execute_tool(name, arguments)
        return {"ok": True, "result": result}
    except TimeoutError:
        # 真实系统可以按策略有限重试;这里不实现重试细节
        return {
            "ok": False,
            "error": {
                "type": "TIMEOUT",
                "message": "工具调用超时"
            }
        }
    except Exception as exc:
        return {
            "ok": False,
            "error": {
                "type": "TOOL_ERROR",
                "message": str(exc)
            }
        }

如果第三方工具返回下面这样的业务错误:

python 复制代码
{
    "code": 789,
    "message": "quota exceeded"
}

Runtime 未必知道 789 代表什么。如果它无法可靠判断错误是否可重试,可以把错误结果交回模型,让模型决定是否换一个工具、调整查询,或者向用户说明限制。

不过,模型提出"再试一次"也不等于 Runtime 必须执行。重试次数、超时、成本和权限仍应由 Runtime 控制。对于明确不可重试的错误,程序应该直接阻止重试,而不是把决定权全部交给模型。

这里可以把错误大致分成三类:

  • 基础设施错误:网络中断、超时、临时服务不可用。Runtime 可以根据明确规则决定是否有限重试。
  • 业务错误:配额不足、账号冻结、权限不足。通常需要工具提供方或业务逻辑给出语义。
  • 模型决策问题:工具返回的结果不符合任务需要,模型可以尝试换一种办法,但仍要受 Runtime 的限制。

业界常见做法不是要求所有工具都用完全相同的业务错误码,而是把调用状态、错误信息和结果结构化,让 Runtime 处理自己能判断的部分,把必要的错误信息作为 Observation 返回给模型。具体协议可能不同;例如 MCP 定义了工具调用及结果表达方式,但不会替所有业务系统理解每一个自定义错误码。

四、Tool 标准化与 Runtime 如何管理 Tool

刚才的例子只有一个 Python 函数,所以用字典就能管理。但如果有的工具是 HTTP API,有的是数据库查询,有的是 Shell 命令,还有的来自远程 MCP Server,Runtime 怎么知道该怎么执行?

关键是:Tool 的定义与执行实现是两回事。 给模型看的仍然是名称、描述和参数结构;Runtime 内部则把名称映射到相应的执行逻辑。

text 复制代码
Tool Definition                 Runtime Registry
  search_web  ───────────────→  Python 函数
  get_weather ───────────────→  HTTP 请求
  query_db    ───────────────→  数据库执行器
  search_repo ───────────────→  MCP Client

因此,Tool Registry 不只是一个"工具名字列表"。在一个简单实现中,它可以保存 Definition 和函数映射;在更复杂的 Runtime 中,还可能保存执行方式、权限、超时、来源等元数据。

python 复制代码
tool_registry = {
    "search_web": {
        "definition": {
            "name": "search_web",
            "description": "搜索互联网资料",
            "parameters": {"type": "object"}
        },
        "executor": search_web,
    }
}

这里没有一种适用于所有 Agent 平台的强制分类,要求每个 Tool 都必须声明自己是 Python、HTTP 或 Shell。不同Agent框架和平台可以设计自己的执行器体系。重要的是,Runtime 必须能把模型请求的 Tool 名称映射到真实执行路径。

五、Tool 越来越多的问题

如果 Agent 只有 search_web、get_weather 这几个工具,把全部 Definition 都交给模型很简单。但假设你的 Agent 已经有几百个工具,甚至接入了数千个第三方工具,还能每次都全部传给模型吗?

这时至少会遇到三个问题:

  1. Context 成本增加:工具描述和参数 Schema 本身也会占用上下文空间。
  2. 选择难度上升:多个工具的功能可能很相似,模型更容易选错。
  3. 维护成本增加:工具越多,权限、版本、参数和可用性管理就越复杂。

可以先把问题简化成一个工具目录:

python 复制代码
tool_catalog = [
    {"name": "search_web", "description": "搜索互联网网页"},
    {"name": "search_news", "description": "搜索近期新闻"},
    {"name": "search_papers", "description": "搜索学术论文"},
    {"name": "search_github", "description": "搜索 GitHub 仓库"},
    # 实际系统中可以有更多工具
]

如果每次都把整个目录连同所有参数 Schema 一起发给模型,随着工具数量增长,开销也会增加。于是自然出现了一个新问题:能不能先从大量工具里找出一小批候选,再只把这些工具交给模型?

这就是 Tool Retrieval 要解决的问题。

六、Tool Retrieval:先找到候选工具,再让模型调用

Tool Retrieval 可以理解为"从工具目录里检索与当前任务有关的工具"。它和 Tool Selection 有关联,但不完全相同:Retrieval 负责从大量工具中缩小范围,Selection 则是在当前可用的候选工具中决定用哪一个。

text 复制代码
1000 个 Tool
    ↓ Tool Retrieval
10 个候选 Tool
    ↓ LLM Tool Selection
选出需要调用的 Tool

6.1 先用最简单的关键词检索

我们先不引入向量数据库。只要把工具名称和描述作为检索文本,用关键词找出相关工具,就能演示基本原理。

python 复制代码
def retrieve_tools(query: str, catalog: list[dict]) -> list[dict]:
    terms = set(query.lower().split())
    scored = []

    for tool in catalog:
        text = f"{tool['name']} {tool['description']}".lower()
        score = sum(1 for term in terms if term in text)
        if score:
            scored.append((score, tool))

    scored.sort(key=lambda item: item[0], reverse=True)
    return [tool for _, tool in scored[:5]]

这是极简版本,不是生产级搜索器:没有处理同义词、相关性排序等问题。实际的工具目录可能会使用关键词搜索、向量检索或混合检索,也可能按类别、权限和可用状态先做过滤。

6.2 让 LLM 自己请求搜索 Tool:search_tool

刚才是程序主动做检索:先从用户需求里提取关键词,再调用 retrieve_tools。但有时程序并不知道用户具体需要什么工具,比如让LLM写一篇文章, LLM分析的第一步是搜索网络, 但程序这时并不知道要发送搜索相关的tool给到LLM。

python 复制代码
def search_tool(query: str) -> dict:
    matches = retrieve_tools(query, tool_catalog)
    return {"matched_tools": matches}

假设用户说:"帮我写一篇关于 AI Agent 的公众号文章,先搜索最近的资料。"

第一轮模型不一定直接调用 search_web,它也可以先返回:

python 复制代码
{
    "name": "search_tool",
    "arguments": {
        "query": "搜索近期 AI Agent 新闻和资料"
    }
}

Runtime 执行 search_tool,得到候选工具。随后,程序把这些候选工具的完整 Definition 加入下一轮模型请求中。模型这时才可以真正调用 search_web、search_news 等工具。

text 复制代码
第一次 LLM:只看到 search_tool
    ↓
请求 search_tool("近期 AI Agent 新闻和资料")
    ↓
Runtime 执行 Tool Retrieval
    ↓
返回候选 Tool Definitions
    ↓
第二次 LLM:看到候选工具
    ↓
调用 search_web(...)
    ↓
Runtime 执行搜索并回传结果

要注意:检索出一个工具的名字,并不等于模型已经获得了调用它的完整定义。 下一轮请求需要提供足够的 Definition 和参数 Schema,模型才能按约定构造调用。

这个设计很适合演示"动态发现工具"的思路。它也带来一些工程问题:比如检索没找到合适工具,模型反复搜索工具却一直不调用,这些都需要通过循环限制和错误处理来控制。

6.3 业界怎么做 Tool Retrieval?

常见方案包括:

  • 关键词检索:简单、可解释,适合目录规模较小或描述写得比较规范的场景。
  • 向量检索:把任务描述和工具描述转成向量,寻找语义相近的工具,适合表达方式不一致的情况。
  • 混合检索:结合关键词与向量结果,再做排序。
  • 分层检索:先找工具类别或命名空间,再从类别中找具体工具。
  • 按需加载:初始只暴露少量工具,模型需要时再搜索并加载完整定义。

现代 Agent 平台也开始支持 Tool Search 或类似机制,不再要求把所有工具定义一次性塞进模型上下文。核心思路是先缩小候选范围,再让模型调用具体工具。

七、Skill 和Tool的关系

接下来我们在聊聊Skill和tool的关系。假设我们有一个"公众号写作 Skill",它告诉 Agent 完成文章的基本流程:

python 复制代码
wechat_writing_skill = {
    "name": "wechat_writing",
    "instructions": [
        "先收集近期资料",
        "整理核心观点",
        "撰写公众号文章",
        "检查并修改文章"
    ]
}

Skill 描述的是一种做事方法或工作流程。但"先收集资料"并没有告诉 Agent 具体该调用哪个搜索工具。如果当前上下文里没有搜索工具,Skill 也不会凭空创造出一个。

因此,需要把 Skill 和 Tool Retrieval 接起来:

text 复制代码
用户要求写文章
    ↓
加载公众号写作 Skill
    ↓
Skill 提示需要搜索资料
    ↓
Tool Retrieval 搜索可用工具
    ↓
向 LLM 提供候选 Tool Definitions
    ↓
LLM 决定调用哪个工具

可以把 Skill 看成"怎么完成任务的指导",把 Tool 看成"实际可调用的能力"。Skill 可以让 LLM 使用多个 Tool,也可能执行Skill自身的脚本。

八、多个 Tool 调用与调度:谁决定串行还是并行?

文章写作往往不止需要一个搜索工具。为了写好一篇关于 AI Agent 的文章,我们可能要搜索近期新闻、学术论文和 GitHub 项目。

text 复制代码
search_news
search_papers
search_github

如果三个搜索任务彼此独立,就可以并行执行;如果后一个任务依赖前一个任务的结果,就必须按顺序执行。模型可能在一次响应中提出多个 Tool Call, Runtime 可以同时执行它们。

8.1 独立任务可以并行

下面用 Python 的 asyncio 展示核心执行方式:

python 复制代码
import asyncio

async def run_tool_async(name, arguments):
    # 实际系统中应接入异步工具实现,并加超时与错误处理
    return await asyncio.to_thread(
        execute_tool, name, arguments
    )

async def run_independent_calls(tool_calls):
    tasks = [
        run_tool_async(call["name"], call["arguments"])
        for call in tool_calls
    ]
    return await asyncio.gather(*tasks, return_exceptions=True)

这段代码假设调用之间互相独立。真实系统还要限制并发数、处理超时和异常,并判断工具是否允许并行执行。asyncio.gather 本身不会替我们分析任务之间的依赖关系。

8.2 有依赖的任务不能盲目并行

比如先搜索资料,再根据搜索结果选出最有价值的内容,最后才能写文章:

text 复制代码
search_material
      ↓
select_evidence
      ↓
write_article

Runtime 如果不知道这些依赖,就无法仅凭三个 Tool Call 的名称判断应该怎么调度。可以让模型输出任务依赖,也可以由程序根据预定义流程控制,比如动态规划和执行图。

8.3 业界怎么处理?

常见方式有:

  • 串行执行:简单可靠,适合有依赖的任务。
  • 并行 Tool Calls:适合互相独立的调用。
  • 程序化调度:Runtime 负责循环、条件分支、并发和中间结果处理。
  • 工作流或执行图:把节点、依赖和执行顺序显式表示出来。

重点是:Tool Calling 解决的是模型如何提出工具调用请求;多个调用怎么排队、并发、重试和控制权限,仍然需要 Agent Runtime 的执行策略。

九、从 Tool Calling 走向 Planning / Orchestration

我们最初只是给 Agent 加了一个 search_web,让它能够完成一次外部操作。但随着需求升级,它需要从几百个工具中找到合适的工具,需要把 Skill 的工作步骤与工具连接起来,还需要决定多个工具的执行顺序、处理失败并管理中间结果。

这条演进路径:Tool 让 Agent 能执行动作;Tool Retrieval 让 Agent 能从大量能力中找到合适的工具;调度让多个工具协同工作;Planning 则进一步决定为了完成目标,需要做哪些步骤、先做什么、后做什么。


本篇小结

这一篇我们学会实现一个简单tool,沿着一个任务,看到 Tool 能力不断增长后带来的工程问题:

  1. Tool Calling 建立了模型决策与程序执行之间的闭环。
  2. Runtime 需要管理工具定义、执行实现和调用错误。
  3. Tool 数量增长后,全部暴露给模型会带来 Context、成本和选择问题。
  4. Tool Retrieval 可以先缩小候选范围,search_tool 则展示了模型主动发现工具的一种设计。
  5. Skill 描述做事方法,Tool 提供实际能力,Runtime 将两者连接起来。
  6. 多个 Tool 的调用需要考虑并行、串行、依赖和错误处理。
  7. 当多步任务需要动态规划与调度时,就自然走向 Planning / Orchestration。

希望这篇文章能对你有所帮助。关注我,持续分享更多实用、好用的 AI 干货。

相关推荐
xcLeigh7 小时前
本体驱动的AI大模型:方法与实践
人工智能·ai·大模型·agent·提示词·语义建模
hpoenixf7 小时前
从工具调用到自适应研究:让 Agent 边查、边算、边调整
agent
网络毒刘7 小时前
Rules 冲突排查:多条规则互相打架时如何用优先级、范围与示例消歧
agent·ai编程·cursor·rules
武子康7 小时前
Cosmos Curator 只跑一条视频,为什么还会加载一串模型?
人工智能·深度学习·agent
网络毒刘7 小时前
端到端:用 Cursor Agent 完成「小功能 + 单测 + PR 描述」并附人工验收清单
单元测试·agent·ai编程·cursor·工具实践
漂着的圆木8 小时前
模型本地沙箱MXC:Copilot Agent工具受限与Ollama发现核对
agent·github copilot·ollama·mxc·工具权限
七夜zippoe8 小时前
多 Agent 协作架构:Supervisor 模式——主管 Agent 调度实战
数据库·ai·架构·agent
是Dream呀8 小时前
Dropout 是暂退法还是丢弃法?我用 TextIn xParse 做了一个术语对账台
人工智能·agent·textin·ai数据层基础设施
JWASX8 小时前
【agent 开发】agent 开发学习 - LangChain(1)
python·学习·agent