2025 年 Q3 我们给客服 agent 接入了第 21 个业务工具,准备晚上发版睡觉。第二天早上 9 点,用户投诉率从平时的 0.4% 飙到 3.8%,客服群被 600 条"机器人答非所问"刷屏。复盘了 5 周,我才真正搞明白:LLM agent 的工具数量不是可以线性堆叠的——从 20 个增到 80 个的过程里,GPT-4 的工具选择准确率从 89% 一路掉到 31%。这篇是我们在生产环境真实踩出来的坑、用过的 4 种修法、最后跑通的分层 + 路由 + 元工具检索三层架构,以及 5 周里走错的 9 个弯路。
背景:一个还算正常的客服 agent
这是一个 ToB 的客服 agent,接给 200+ 中小企业用,日均 12 万次对话,后端是 GPT-4o + LangGraph 0.2,部署在 K8s 三可用区里,平均 p95 延迟在事故前一直稳定在 1.4 秒以下。第一版上线时只有 6 个工具:查订单、查物流、退款发起、退款查询、商品搜索、转人工。那时候我们用最朴素的 tools= 数组直接塞到 chat completion 里,选择准确率(用人工标注 500 条对话验证)有 94%。这个数字让团队对"LLM 工具调用"产生了一种过度乐观的印象,大家觉得"反正都是 GPT-4 自己理解,加工具就是改 schema 的事",这个错觉为后面埋了非常大的雷。
随着客户接得越来越多,工具数也滚雪球式增长:6 → 12 → 21 → 38 → 51 → 80。每次增加都是某个客户提的需求,产品同学说"这个工具特别简单,大模型肯定能理解",我们也就这么塞进去了。复盘下来,这种"按需添加"的工作方式本身就是问题:每次只看新工具相对于原系统的边际影响,没人有动力去做整体回归测试,也没人有动力下线那些边缘工具——因为下线一个工具要去跟产品 PM、客户成功、甚至客户本人解释,而新增工具几乎不需要解释。最终,直到那次 21 个工具的发版,事故才把这个慢性病炸出来。
事故发生时我才意识到,我们对"agent 工具数"完全没有任何量化的护栏。同样大小的服务,DB 表数量超过 200 个、redis key 总数超过千万,我们都有报警、有 SOP、有 review checklist。但工具数从 6 涨到 80 这件事,居然没有任何门槛、没有 review、没有 capacity planning。这种治理空白本身,比任何架构问题都更值得反思。
事故时间线
| 时刻 | 事件 | 关键指标 |
|---|---|---|
| T-7 天 | 新增 3 个工具:查询发票、申请发票、修改发票抬头 | 工具总数 18 → 21 |
| T-0(发版当晚) | 灰度 5% 流量,无明显异常,全量 | 工具调用错误率 0.6% |
| T+9h(次日 9:00) | 客服群开始爆,用户投诉"问退款给我查发票" | 错误率飙到 3.8% |
| T+10h | 初步定位是工具选错,先把 3 个发票工具下掉 | 错误率回落到 1.1% |
| T+12h | 下午回到 0.7%,但仍高于发版前 | 留下技术债 |
| T+1 周 | 批了 5 周时间做根因分析 + 架构改造 | 立项 |
| T+5 周 | 新架构灰度跑稳,工具数扩到 80 个,准确率回到 91% | 结案 |
第一轮排查:我们以为是 prompt 问题
事故当天 10 点回滚后,值班同学第一反应是"肯定是新工具的描述写得不好"。我们对比了 3 个发票工具的 description,确实有点啰嗦,改短改清楚,重发。结果第二天又收到投诉,这次是"问发票给我查物流"——错的方向反过来了。也就是说新加的工具没"抢"走老工具的请求,但老工具反而开始"抢"新工具的请求。这非常反直觉。
我们意识到这不是单个 description 的问题,是模型在 21 个 description 里整体的"理解能力"下降了。于是开始做一个完整的离线评估集——这件事其实应该一开始就做。
import json, openai, asyncio
from typing import List, Dict
# 评估集:500 条人工标注的真实对话,每条带 ground truth 工具名
EVAL_SET = json.load(open("eval_500.json"))
async def eval_tool_selection(tools: List[Dict], samples: List[Dict]) -> float:
"""跑一遍评估集,返回工具选择准确率"""
client = openai.AsyncOpenAI()
correct = 0
total = len(samples)
sem = asyncio.Semaphore(20)
async def _one(item):
async with sem:
resp = await client.chat.completions.create(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": item["user"]},
],
tools=tools,
tool_choice="auto",
temperature=0,
)
msg = resp.choices[0].message
if msg.tool_calls and msg.tool_calls[0].function.name == item["expected_tool"]:
return 1
return 0
results = await asyncio.gather(*[_one(s) for s in samples])
return sum(results) / total
# 跑出来:21 个工具时准确率 78%,12 个工具时是 89%
acc_12 = asyncio.run(eval_tool_selection(TOOLS_12, EVAL_SET))
acc_21 = asyncio.run(eval_tool_selection(TOOLS_21, EVAL_SET))
print(f"12 tools: {acc_12:.2%}, 21 tools: {acc_21:.2%}")
这一跑就把现实摆在眼前:21 个工具下整体准确率只有 78%,而 12 个工具的时候有 89%。多了 9 个工具,损失了 11 个百分点。我们突然意识到,如果不解决这个问题,后续根本扩不到 50 个、80 个工具。
更严重的是,我们以前完全没有这套评估集。3 天里建评估集的过程花了 4 个人 22 小时,扒了过去 30 天的真实对话,人工标注 500 条,每条都标了"应该调哪个工具""理想参数应该是什么""理想回复关键词"。这个评估集后来变成我们整个 agent 团队最重要的资产——这次复盘最大的回报其实就是这个评估集本身,它让后面的每一次改动都有"通过 / 不通过"的客观标尺,而不是各凭直觉吵。
顺带说一句,我们对评估集做过一次"模型抖动"测试:同一个 prompt、同一个 query、temperature=0 跑 5 遍,GPT-4o 仍然有大约 1-2% 的样本会给出不同的工具选择。这意味着评估指标天然有 ±1.5 个点的噪音,任何小于 2 个点的"提升"都不能算真的提升,需要连跑 3 次取均值再判断。这一点对后续做 A/B 实验帮助很大,避免了团队拿"偶然好看的数据"上线变更。
第二轮排查:把工具数当变量做了压力测试
真正想明白问题,是后来我们做了一组"工具数量 vs 准确率"的曲线——把 80 个工具按业务分组,然后随机抽取 5/10/20/40/80 个塞到 tools 数组里,跑同一个评估集。
| 工具数 | 选择准确率 | 平均 tokens/调用 | 平均延迟 |
|---|---|---|---|
| 5 | 96% | 1100 | 720ms |
| 10 | 93% | 1850 | 840ms |
| 20 | 89% | 3200 | 1.1s |
| 40 | 71% | 5800 | 1.6s |
| 60 | 49% | 8100 | 2.2s |
| 80 | 31% | 10500 | 2.9s |
曲线非常清晰:工具数过了 20 之后,准确率开始陡降;过了 40 进入崩塌区。同样的现象在 Claude 3.5 Sonnet 和 Gemini 1.5 Pro 上都验证了,数字略有差异但趋势一致——Claude 在 80 工具下仍能维持到 37%,Gemini 1.5 Pro 是 34%,差距没有想象中显著。这不是单个模型的问题,是大模型在 long context 选择题里的一个共性弱点,跟具体 vendor 关系不大。
更值得注意的是 tokens 数:80 个工具光 schema 就占 1 万多 tokens,每次对话都要带着这堆 schema 进去,成本和延迟也是问题。我们一度想过"那就只塞跟当前问题相关的工具"——这个朴素直觉后来变成了正解的一部分,但实现起来比想象中复杂得多。具体来说,所谓"相关"需要在 query 级别动态决策,意味着每一次对话都要做一次额外的语义匹配,这件事如果用 LLM 来做,延迟和成本就两头都堵死;如果用 embedding,又会面临"近义工具仍然区分不开"的老问题。我们后来落地的方案融合了 embedding 检索 + LLM 二次确认 + 兜底元工具,后面会详细讲。
还有一个我们后来才意识到的问题:工具数膨胀不光影响准确率,还显著降低了系统的"可调试性"。当一个对话调错工具,我们想知道为什么调错,只能去看那 80 个 schema 拼出来的 prompt——光是把这个 prompt 渲染出来人工读一遍就要 10 分钟。工具数越多,故障复盘的人均成本越高,SRE 同事最早抱怨的就是这一点。
问题本质:为什么工具一多就崩
我们查了 OpenAI 和 Anthropic 的几篇技术博客,加上自己抓 trace 分析,最后总结出 4 个因果:
第一,语义相近的工具之间互相"污染"。比如"查询订单状态"和"查询发票状态",对模型来说在 embedding 空间里几乎重叠,描述里只要有一处轻微的歧义,模型就会随机选其中一个。当工具池里有 5 对这种近义工具时,错误率就会乘积式爆炸。
第二,prompt 中后段的工具被"遗忘"。tool schema 是按顺序拼到 prompt 里的,模型有典型的 lost-in-the-middle 倾向,排在 30 名以后的工具会被显著低权重对待。我们做过一个实验:把同一个工具放在 list 的第 1、20、40、60 位,选择召回率分别是 0.95、0.81、0.62、0.43。
第三,description 写法不一致带来的解析负担。早期工具的 description 是简练的命令式("查询订单"),后来的工具 description 越写越长,有人写背景、有人写返回值、有人写适用场景。模型在解析这种"风格不一"的 80 段描述时,真的会变笨。
第四,默认参数 vs 必填参数的混乱。当 20 个工具同时存在,且都有 3-5 个参数时,模型会出现"参数张冠李戴"的现象——比如把订单号填到了发票号字段里。这部分错误其实不是工具选错,但用户感知一致:"机器人答非所问"。
问题示意图
四种修法的取舍
修法 1:Prompt 工程优化(治标不治本)
第一周我们试的是 prompt 工程:统一所有工具 description 的格式,加 few-shot 例子,在 system prompt 里强调"如果是退款问题优先选 refund_initiate"。这条线确实把 21 个工具的准确率从 78% 拉回到 84%,但工具数扩到 40 时又掉到 75%,继续扩根本扛不住。
结论:prompt 工程是必要的清洁工作,但不是架构方案。我们用半天时间把所有 description 重写成统一模板,这个收益是免费的、应该做的,但不能依赖它解决问题。
# 统一后的 description 模板
TOOL_DESC_TEMPLATE = """
{verb}{object}。
[何时调用] {when_to_call}
[何时不调用] {when_not_to_call}
[参数说明] {param_summary}
[返回示例] {return_example}
"""
# 示例:
{
"name": "refund_initiate",
"description": (
"发起一笔退款。\n"
"[何时调用] 用户明确说要退款、退货、不要了、申请退款。\n"
"[何时不调用] 用户只是查询退款进度、问退款规则、问能不能退——这些走 refund_query 或 refund_policy。\n"
"[参数说明] order_id 必填,reason 必填(从枚举里选)。\n"
"[返回示例] {{\"refund_id\": \"R20251030001\", \"status\": \"submitted\"}}"
),
"parameters": {...}
}
修法 2:工具分层(按场景切分子 agent)
这是我们真正动结构的第一刀:不再让一个 agent 看到所有 80 个工具,而是按业务把 agent 拆成 6 个"领域 agent",每个 agent 只持有自己领域的 8-15 个工具。
| 领域 agent | 工具数 | 典型工具 |
|---|---|---|
| 订单类 | 11 | 查订单、改地址、催发货、查物流、确认收货... |
| 售后类 | 13 | 退款发起、退款查询、退货寄回、补偿券发放... |
| 商品类 | 9 | 商品搜索、库存查询、规格对比、推荐... |
| 账户类 | 8 | 登录、绑定手机、修改密码、注销... |
| 发票类 | 7 | 查发票、申请发票、改抬头、合开发票... |
| 通用类 | 12 | 转人工、留言、催回复、记录工单... |
上层是一个轻量的"路由 agent",只负责判断当前问题属于哪个领域,然后转到对应子 agent。路由 agent 看到的不是 80 个工具,而是 6 个"领域选择"。
from langgraph.graph import StateGraph, END
from typing import TypedDict, List
class AgentState(TypedDict):
messages: List[dict]
domain: str | None
tool_results: List[dict]
DOMAINS = ["order", "after_sales", "product", "account", "invoice", "common"]
ROUTER_PROMPT = """你是一个客服 agent 的路由器。请根据用户当前问题判断属于哪个领域,
只回答领域名(order/after_sales/product/account/invoice/common),不要解释。
例子:
"我要退款" -> after_sales
"我的快递到哪了" -> order
"开张发票" -> invoice
"转人工" -> common
"""
async def router_node(state: AgentState) -> AgentState:
resp = await client.chat.completions.create(
model="gpt-4o-mini", # 路由不需要大模型
messages=[
{"role": "system", "content": ROUTER_PROMPT},
{"role": "user", "content": state["messages"][-1]["content"]},
],
temperature=0,
max_tokens=20,
)
domain = resp.choices[0].message.content.strip().lower()
if domain not in DOMAINS:
domain = "common"
state["domain"] = domain
return state
# 子 agent 只持有自己领域的工具
DOMAIN_TOOLS = {
"order": ORDER_TOOLS, # 11 个
"after_sales": AFTER_TOOLS, # 13 个
"product": PRODUCT_TOOLS, # 9 个
"account": ACCOUNT_TOOLS, # 8 个
"invoice": INVOICE_TOOLS, # 7 个
"common": COMMON_TOOLS, # 12 个
}
async def domain_agent_node(state: AgentState) -> AgentState:
tools = DOMAIN_TOOLS[state["domain"]]
resp = await client.chat.completions.create(
model="gpt-4o-2024-08-06",
messages=[{"role": "system", "content": DOMAIN_PROMPTS[state["domain"]]}] + state["messages"],
tools=tools,
tool_choice="auto",
temperature=0,
)
# ... 处理 tool_calls
return state
graph = StateGraph(AgentState)
graph.add_node("router", router_node)
graph.add_node("domain", domain_agent_node)
graph.set_entry_point("router")
graph.add_edge("router", "domain")
graph.add_edge("domain", END)
app = graph.compile()
这一刀下去,准确率从 71%(40 个工具 flat 模式)涨到 85%。但出现了新问题:跨领域请求处理不好。比如"我退款了但发票已经开了怎么办"——这同时跨售后和发票,路由 agent 只能选一个,选错了就转不回来。我们后来用 LangGraph 的多 node 协同 + 状态共享解决了一半,但仍有约 6% 的请求是真·跨域。
修法 3:工具检索(用 embedding 做相关性筛选)
分层之后,我们想再优化掉跨域不准的问题,引入了第二层:在子 agent 内部,如果某个领域的工具超过 10 个,就先用 embedding 检索把跟当前问题最相关的 5-7 个工具挑出来,再塞给 LLM。
import numpy as np
from openai import AsyncOpenAI
class ToolRetriever:
"""对工具 description 做 embedding,运行时根据 query 检索 top-k 工具"""
def __init__(self, tools: List[dict], embed_model: str = "text-embedding-3-small"):
self.tools = tools
self.embed_model = embed_model
self.client = AsyncOpenAI()
self.embeddings: np.ndarray | None = None
async def build(self):
# 给每个工具构造一个"检索文本":name + description + 典型 query 示例
texts = [
f"{t['function']['name']}: {t['function']['description']}\n"
f"示例问句:{'; '.join(t.get('sample_queries', []))}"
for t in self.tools
]
resp = await self.client.embeddings.create(model=self.embed_model, input=texts)
self.embeddings = np.array([d.embedding for d in resp.data])
async def retrieve(self, query: str, top_k: int = 6) -> List[dict]:
q_resp = await self.client.embeddings.create(
model=self.embed_model, input=[query]
)
q_vec = np.array(q_resp.data[0].embedding)
sims = self.embeddings @ q_vec / (
np.linalg.norm(self.embeddings, axis=1) * np.linalg.norm(q_vec) + 1e-9
)
top_idx = np.argsort(-sims)[:top_k]
return [self.tools[i] for i in top_idx]
# 用在 domain agent 里
retriever = ToolRetriever(DOMAIN_TOOLS["after_sales"])
await retriever.build()
async def smart_domain_agent(state: AgentState) -> AgentState:
query = state["messages"][-1]["content"]
candidate_tools = await retriever.retrieve(query, top_k=7)
resp = await client.chat.completions.create(
model="gpt-4o-2024-08-06",
messages=state["messages"],
tools=candidate_tools, # 只塞 7 个,不是 13 个
tool_choice="auto",
)
return state
有几个细节决定了检索效果:
- "检索文本"不能只用 description——业务工具的 name 很重要,而且每个工具我们手动写了 5-10 条 "样例问句",一起拼到 embedding 里。这一步让召回提升了 12 个百分点。
- 始终保留 1-2 个"兜底工具"(比如转人工、留言),不参与检索筛选,无条件塞进去。否则有些边缘问题模型会"无工具可用"。
- 检索的 top-k 不要太小。我们试过 k=3,有些时候确实选不到正解;k=7 是经验最优值,k=10 又开始回退到 lost-in-middle 问题。
- embedding 离线预计算 + 内存缓存。工具 description 一天更新不了几次,完全可以服务启动时一次性算好。线上 query embedding 走 batched cache,99% 的常见问句直接命中。
修法 4:元工具检索(让 LLM 自己"查工具手册")
这是我们最后一层、也是最反直觉的一招。当工具数过了 60,即使每个领域内做了 embedding 检索,有时候 LLM 还是会卡——因为它根本不知道有这个工具存在。比如用户问"我能不能把多张发票合开成一张",系统里的 invoice_merge 工具被检索丢了,LLM 就只会回答"暂不支持"。
我们后来做的方案是:给 LLM 一个元工具叫 search_tools(query),让它在不知道有没有合适工具时主动调用这个元工具去搜索。返回的不是结果,而是匹配的工具列表 + 调用说明。LLM 再根据这个返回去发起真正的工具调用。
META_TOOL = {
"type": "function",
"function": {
"name": "search_tools",
"description": (
"在不确定有没有合适工具能解决用户问题时,先调用本工具搜索可用工具。"
"返回最相关的 3-5 个工具说明。"
"[何时调用] 用户的问题不能直接匹配到你已知的工具时。"
"[何时不调用] 已经能确定要用哪个工具时,直接调那个工具就行。"
),
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "用自然语言描述需要的工具能力,例如:把多张发票合并成一张"
}
},
"required": ["query"]
}
}
}
# 后端实现
async def handle_search_tools(query: str) -> str:
hits = await GLOBAL_RETRIEVER.retrieve(query, top_k=5)
lines = []
for h in hits:
lines.append(
f"- {h['function']['name']}: {h['function']['description'][:120]}"
)
return "可用工具:\n" + "\n".join(lines) + "\n请直接调用其中合适的一个。"
元工具检索让 80 个工具下的覆盖率从 76% 涨到了 91%。代价是平均多一次模型调用(从 1.2 次平均到 1.4 次),延迟和 token 成本略涨,但对长尾问题的"识别能力"是质变。
三层架构最终形态
5 周里走错的 9 个弯路
- 第 1 周想用更强的模型解决——把 gpt-4o 换成 gpt-4-turbo 又换回 claude-3.5-sonnet,效果都没本质提升。模型不能解决工具空间设计问题。
- 第 1 周末重写 description——确实有效但单独无法支撑 40+ 工具。
- 第 2 周想用 function chaining——把工具拆得更细更原子,结果反而工具数翻倍,准确率更糟。原子化是反方向。
- 第 2 周中尝试 self-reflection——让 LLM 选完工具后再反思一次,可以从 78% 升到 82%,但成本翻倍延迟翻倍,产品不接受。
- 第 3 周做工具分组但没做路由——把工具按领域贴标签,在 system prompt 里告诉 LLM "退款相关请优先看 #refund 标签",效果约等于不变,因为 LLM 不会真的"过滤"。
- 第 3 周末走对了分层路线——但路由 agent 用 gpt-4o 太贵,后来换 gpt-4o-mini + few-shot 提示,准确率 96%,成本降 90%。
- 第 4 周做 embedding retrieval 没加 sample queries——只用 description 做向量,召回率只到 79%,加上人工写的样例问句之后冲到 91%。
- 第 4 周中检索 top-k 想做动态——根据 query 长度自适应 k,结果工程复杂度上升收益不到 1 个点,回退到固定 k=7。
- 第 5 周想去掉元工具——觉得"分层 + 检索"已经够好了,试着把元工具拿掉,长尾覆盖率立刻从 91% 掉到 84%,加回来。
评估体系是基础设施,不是事后补救
这次复盘最大的认知改变,其实不是上面那些架构招式,而是:没有持续运行的离线评估集,任何 agent 优化都是赌博。我们后来沉淀了一个 1500 条规模的评估集,每次工具增删、prompt 改动、模型升级,CI 里自动跑一遍:
# .github/workflows/agent_eval.yml
name: Agent Eval
on:
pull_request:
paths:
- 'agents/**'
- 'tools/**'
- 'prompts/**'
jobs:
eval:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install
run: pip install -r requirements.txt
- name: Run eval set
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: python -m eval.run --set datasets/eval_1500.jsonl --output report.json
- name: Check thresholds
run: |
python -c "
import json
r = json.load(open('report.json'))
assert r['tool_selection_acc'] >= 0.88, f\"acc {r['tool_selection_acc']} below 0.88\"
assert r['cross_domain_acc'] >= 0.80, f\"cross-domain {r['cross_domain_acc']} below 0.80\"
assert r['p95_latency_ms'] <= 2500, f\"p95 {r['p95_latency_ms']} above 2500\"
"
- name: Upload
uses: actions/upload-artifact@v4
with:
name: eval-report
path: report.json
评估集需要包含:正向样例(每个工具至少 10 条)、近义混淆样例、跨域样例、无法用任何工具回答的样例(测试 fallback)、参数提取样例。这五类样本缺一个,评估就有漏洞。
性能 / 成本对比
| 方案 | 准确率 | p95 延迟 | 每次对话 token | 每次对话 USD |
|---|---|---|---|---|
| 原始 flat(80 工具) | 31% | 2.9s | 10500 | 0.052 |
| 仅 prompt 优化 | 43% | 2.7s | 10100 | 0.051 |
| 分层(6 子 agent) | 78% | 1.8s | 4200 | 0.024 |
| 分层 + 检索 top-k 7 | 87% | 1.6s | 2800 | 0.018 |
| 三层(+ 元工具) | 91% | 1.9s | 3300 | 0.021 |
三层架构相比原始 flat 模式,准确率 31% → 91%,token 消耗 10500 → 3300,成本下降 60%。多了 0.2 秒元工具调用,但准确率换得值。
决策树:什么时候用哪种方案
我们立的 9 条 agent 工程纪律
- 工具数 > 20 必须有离线评估集,> 40 必须接 CI,任何 PR 不过线就不准合。
- 新工具上线前必须先标 5-10 条样例问句,跟 description 一起作为评估和检索的输入。这是产品同学的义务,工程同学拒接没有样例的工具。
- tool description 必须用统一模板:[何时调用] / [何时不调用] / [参数说明] / [返回示例]。新人提交的工具如果没按模板写,CI 检查脚本直接拒。
- 语义相近工具必须在描述里明确划界:"refund_initiate 和 refund_query 的区别是…",必要时加入反面例子。
- 路由层永远用便宜小模型(gpt-4o-mini / claude-haiku),决策层才用大模型。不要让大模型干路由的活,贵且未必更准。
- 始终保留兜底工具(转人工 / 留言 / 反馈),不参与检索过滤,无条件可用。
- 不要相信单次评估,所有指标必须连跑 3 次取均值,LLM 输出有抖动。
- 工具增删要走"投票制":新工具上线必须证明评估集准确率不下降;不下降才能进生产。
- 每月做一次工具"瘦身审计":近 30 天调用次数 < 50 的工具进入观察名单,< 10 的工具下线或合并。我们 5 周里下掉了 11 个"看上去有用但其实没人用"的工具,这件事对模型选择准确率的提升远比想象中大。
关于成本的一笔账
很多人忽略 token 成本。我们每天对话量 12 万次,优化前每次 0.052 美元,日均 6240 美元,月 19 万美元。优化后每次 0.021 美元,日均 2520 美元,月 7.6 万美元。5 周的架构改造每年省下约 130 万美元的 OpenAI 账单。后端 4 个工程师做这件事的工资,加在一起还不到这个数字的 1/10。
当然这是按 GPT-4o 当前定价算的,后续我们也在评估把路由 agent 和部分子 agent 切到 Claude Haiku 或 Gemini Flash,理论上还能再压 40%。但在没把架构理顺之前,光换模型救不了你——这是这次复盘最坚硬的一条经验。
总结
从 21 个工具崩到 80 个工具稳,我们花了 5 周和大约 90 小时的工程师时间,踩了 9 个弯路,最终落地的三层架构其实并不复杂:路由分流到领域子 agent,子 agent 用 embedding 检索过滤工具,LLM 找不到合适工具时调用元工具 search_tools 主动查手册。复杂的不是架构,而是承认大模型"工具空间"是一种 context,会像 long context 一样有上限。
这之后我自己写新 agent 时,工具数一过 15 就先把分层骨架搭起来——不是因为现在需要,而是知道再加几个就会需要。架构上的"过早优化"在 agent 工程里其实是节约,你后期再回头改要重写 prompt、重写评估集、重新做灰度,代价比预想的高得多。如果你正在做一个工具规模化扩张的 agent,希望这 5 周的弯路能帮你少走几个。
—— 别看了 · 2026