Agent 的工具选择策略:从硬编码到动态决策
引言
早期 Agent 的工具选择是 if intent=="weather": call_get_weather():人写路由,模型只填参数。这种做法可预测、好测试,但工具一多就崩------加一个"汇率查询"就要改分支,加 50 个工具就变成意大利面。
新一代 Agent 把工具选择交给"模型 + 工具元数据 + 运行时上下文":模型在解码时看用户话、工具描述、历史状态、权限边界,决定调谁、调不调、先调谁后调谁。MCP 官方把工具定义为"模型可发现、可调用"的原语;工具由模型控制,但是否给人确认由实现决定。 OpenAI Agents SDK 用 tool_choice: auto/required/none/具体工具名 把"模型自由选"和"系统强制选"放在同一层。
所以工具选择的演进不是"模型代替代码",而是:静态路由 → 模型决策 → 策略网关 → 动态发现 的四层混合。
技术背景
工具选择有三个时代:
- Hardcoded Routing :规则/状态机决定工具,模型只做 NLU
优点:可解释、零幻觉调用
缺点:不可扩展、不能处理"没见过的组合意图" - LLM Function Calling / Tool Use :工具 schema 进上下文,模型输出
tool_use
优点:自然语言直达工具,多工具动态组合
缺点:工具多了有"上下文税",模型会选错、重复选、漏选 - Dynamic Tool Ecosystem(MCP + Agent Registry + Guardrail) :工具在 MCP Server / 工具市场里运行时发现,模型从"可用工具集"里决策,执行前过权限/预算/人工确认
优点:N 个客户端 × M 个工具变成 N+M,跨模型跨框架复用
缺点:发现层、鉴权层、描述质量、决策可观测性都得自己补
Anthropic 写工具文档时说得直白:工具描述怎么写,会显著改变 Agent 行为;小改动大影响。 也就是说,动态决策的质量,一半在模型,一半在工具说明书。
应用使用场景
- 企业助手:HR/IT/财务/CRM 工具几百个,不能全塞进 system prompt
- 研发 Agent:读代码、跑测试、查 CI、建 PR、调 Jira,工具按仓库/权限动态加载
- 客服 Agent:订单、物流、退款、知识库、工单系统,按用户身份和会话阶段裁剪
- 数据 Agent:BI、数仓、Excel、Python、向量库,按语义检索相关工具而非全量暴露
- 跨组织 Agent:A 公司 Agent 连 B 公司 MCP Server,工具目录运行时协商,不写死端点
不同场景下详细代码实现
场景一:企业助手------50 个工具全塞进去,模型开始乱点
企业诉求 :某 SaaS 厂商给客户做"内部助手",一开始把 HR、CRM、财务、运维 52 个函数全挂上。结果用户问"我的年假剩几天",模型顺手调了 create_invoice 和 reboot_server------虽然没权限,但吓出一身冷汗。
难点:工具越多,模型越容易被名字带感的工具带偏;"看起来相关"≠"该调";全量工具让上下文又贵又吵。
MCP 官方建议:工具由模型控制、可运行时发现;但实现层 SHOULD 给人确认、UI 要明确展示"模型能用哪些工具"。
OpenAI 官方建议 :用 tool_choice=auto 让模型选,但高权限动作要走 guardrail / approval,不是只靠 prompt。
我是怎么做的:加"工具路由层":
- 先用语义检索从 52 个工具里召回 Top-5 候选
- 再让模型在候选里选
- 写操作 / 跨域操作 / 高花费操作进
requires_approval - 最终执行前跑
policy_gate
python
# tool_router.py
TOOLS = {
"get_leave_balance": {"domain":"hr","risk":"low","desc":"查年假余额"},
"create_invoice": {"domain":"finance","risk":"high","desc":"开发票"},
"reboot_server": {"domain":"ops","risk":"critical","desc":"重启服务器"},
"search_kb": {"domain":"support","risk":"low","desc":"搜知识库"},
}
def retrieve_tools(query, k=5):
# 真实用 embedding;这里用关键词重叠偷懒演示
return sorted(TOOLS.items(),
key=lambda kv: sum(w in query for w in kv[1]["desc"].split()))[:k]
def policy_gate(tool_name, user_role):
meta = TOOLS[tool_name]
if meta["risk"] == "low":
return "AUTO"
if meta["risk"] == "high" and user_role in ("finance","admin"):
return "APPROVAL"
if meta["risk"] == "critical":
return "BLOCKED_NON_SRE"
return "APPROVAL"
q = "我年假还有几天"
cands = retrieve_tools(q)
chosen = "get_leave_balance" if "get_leave_balance" in dict(cands) else cands[0][0]
print("candidates:", [c[0] for c in cands])
print("chosen:", chosen, "->", policy_gate(chosen, "engineer"))
场景二:研发 Agent------MCP 动态发现 Git / CI / 数据库工具
企业诉求 :某研发平台希望 Agent 接 GitHub、Jenkins、Postgres、Jira。团队不想每加一个系统就改 Agent 代码;又怕 Agent 在"看代码"时偷偷 force push。
难点:硬编码 SDK 调用 = 每套系统写一套;纯 MCP 动态发现 = 模型可能选错工具或参数越权。
MCP 官方架构 :Agent 连 MCP Server → tools/list 拿目录 → 模型按 schema 决策 → tools/call 执行;Server 侧管 auth,Client 侧管推理。
OpenAI Agents SDK 做法 :Agent 可以挂 mcp_servers,SDK 把 MCP 工具当普通工具编排。
我是怎么做的 :MCP 负责"有哪些工具 / 怎么调",Agent 负责"现在该调哪个",中间加动作分类器:
- read 类:自动
- write 类:预览 diff + 人确认
- destructive 类:
force push/drop table默认禁,白名单仓库才可谈
python
# mcp_dev_agent.py
class DevToolPolicy:
DESTRUCTIVE = {"force_push", "drop_table", "delete_branch", "purge_ci"}
def decide(self, tool_name, args, repo_trust="normal"):
if tool_name in self.DESTRUCTIVE:
return "BLOCK" if repo_trust != "golden" else "APPROVAL"
if tool_name in ("create_pr", "post_comment", "update_jira"):
return "PREVIEW_THEN_APPROVAL"
return "AUTO"
pol = DevToolPolicy()
print(pol.decide("read_file", {}))
print(pol.decide("force_push", {}, repo_trust="normal"))
print(pol.decide("create_pr", {"title":"fix auth"}, repo_trust="normal"))
场景三:客服 Agent------按会话阶段动态切换工具集
企业诉求:某客服中台发现:一上来就把"退款、封号、改合同"工具给模型,模型容易过早走重动作;而用户其实还在"查订单"阶段。
难点 :不是工具不对,是时机不对。同一条话在阶段 1 是闲聊,在阶段 3 是退款指令。
Anthropic 思路:Planner / tool use 要配合清晰指令和状态;别让一个 Agent 同时当接待、裁判、执行员。
我的做法:会话状态机切"工具剖面(tool profile)":
triage:只读工具(订单状态、知识库、物流)resolve:加"改地址、补发票"等中风险工具escalation:加"退款、补偿、封禁",但必须人工点确认- 每次状态迁移重新组装可用工具列表,不靠模型自制边界
python
# session_tool_profile.py
PROFILES = {
"triage": ["get_order", "search_kb", "get_logistics"],
"resolve": ["get_order", "search_kb", "update_address", "request_invoice"],
"escalation": ["get_order", "refund_order", "compensate_wallet", "block_account"],
}
APPROVAL_REQ = {"refund_order", "compensate_wallet", "block_account"}
def active_tools(state): return PROFILES[state]
def call_in_state(state, tool):
if tool not in PROFILES[state]:
return f"REJECT: {tool} 不在 {state} 阶段工具集"
if tool in APPROVAL_REQ:
return f"APPROVAL_REQUIRED: {tool}"
return f"AUTO: {tool}"
print(call_in_state("triage", "refund_order"))
print(call_in_state("resolve", "update_address"))
print(call_in_state("escalation", "refund_order"))
原理解释
工具选择本质是一个受约束的决策问题:
makefile
输入: 用户话 + 历史 + 用户角色 + 会话状态 + 可用工具集
↓
Layer 0 静态路由 : 规则命中就走,不调模型
Layer 1 语义召回 : embedding / 关键词 / 领域标签 缩成候选集
Layer 2 模型决策 : LLM 看 candidate schema 选 tool + 填 args
Layer 3 策略网关 : 权限 / 风险 / 成本 / 幂等 / 人工确认
Layer 4 执行与回写 : 跑工具、存轨迹、更新可用工具/状态
模型不是"工具选择器"本身,而是 Layer 2 的打分器;真正让系统不疯的是 Layer 1 和 Layer 3。
MCP 解决"工具从哪来、怎么描述、怎么调用";Agent 框架解决"怎么编排、怎么终止、怎么交接";企业系统解决"谁能调、调了算谁的责任"。
核心特性
- 动态发现 :工具目录运行时拉取,不写死在代码里(MCP
tools/list) - 候选裁剪:全量 200 个工具 ≠ 都进上下文,按域/意图/阶段召回
- 风险分级:read / write / destructive / cross_domain 走不同通道
- 强制策略 :
tool_choice控制"必须调 / 随便 / 不准调 / 只调某个" - 人工在环:高危工具给 UI 确认,不是只靠模型良心
- 可观测:哪轮看了哪些工具、为什么选 A 不选 B,必须能回放
- 可回滚:写操作带 preview / dry-run / 幂等键
原理流程图(纯文本)
rust
User Query
│
▼
Session State / Role / Cost Budget
│
▼
Tool Source Layer
├─ Hardcoded tools
├─ Function-calling tools
└─ MCP servers -> tools/list -> unified registry
│
▼
Candidate Retriever (embedding / tag / phase)
│ Top-K tools
▼
LLM Tool Decision
tool_choice: auto / required / none / specific
│
▼
Policy Gate
├─ read/low risk -> AUTO
├─ write/medium -> PREVIEW + APPROVAL
├─ destructive -> BLOCK / whitelist
├─ cross_domain -> extra auth scope
└─ cost/too_many_calls-> throttle / escalate
│
▼
Tool Execution (tools/call / function / API)
│
▼
Observe Result -> update state -> maybe re-plan
│
▼
Audit Log: query, candidates, chosen, args, policy, human_signoff
环境准备
ini
pip install openai anthropic mcp fastmcp redis
# 工具源: 本地 function / OpenAI tool / Anthropic tool / MCP Server
# 召回: sentence-transformers 或向量库(Qdrant/PGVector)
# 编排: OpenAI Agents SDK / LangGraph / Claude Agent SDK
# 策略: Redis 存 session profile,Postgres 存 tool audit
# 护栏: approval service, scoped token, dry-run wrapper
实际详细应用代码示例实现
python
# dynamic_tool_runtime.py
class DynamicToolRuntime:
def __init__(self, registry, retriever, policy):
self.registry = registry # name -> tool meta
self.retriever = retriever # query -> [tool names]
self.policy = policy # (tool,ctx) -> AUTO/APPROVAL/BLOCK
def select_and_run(self, query, ctx):
candidates = self.retriever(query)
# 模型决策用伪函数:真实环境把 candidates schema 给 LLM
chosen = self.llm_pick(query, candidates, ctx)
decision = self.policy(chosen, ctx)
if decision == "BLOCK":
return {"tool": chosen, "status": "blocked", "reason": "policy"}
if decision == "APPROVAL":
return {"tool": chosen, "status": "pending_human",
"preview": self.registry[chosen].get("preview")}
return {"tool": chosen, "status": "executed",
"result": self.registry[chosen]["run"](ctx)}
def llm_pick(self, query, candidates, ctx):
# 最小启发:候选里挑 risk 最低且名字最相关
scored = [(c, self.registry[c]["risk_rank"]) for c in candidates]
return min(scored, key=lambda x: x[1])[0]
运行结果
vbnet
candidates: ['get_leave_balance', 'search_kb', 'create_invoice', 'reboot_server']
chosen: get_leave_balance -> AUTO
AUTO: read_file
BLOCK
PREVIEW_THEN_APPROVAL
REJECT: refund_order 不在 triage 阶段工具集
AUTO: update_address
APPROVAL_REQUIRED: refund_order
测试步骤以及详细代码
scss
# test_tool_selection.py
def test_low_risk_auto():
assert policy_gate("get_leave_balance", "engineer") == "AUTO"
def test_critical_blocked_for_normal_user():
assert policy_gate("reboot_server", "engineer") == "BLOCKED_NON_SRE"
def test_destructive_blocked():
pol = DevToolPolicy()
assert pol.decide("force_push", {}, "normal") == "BLOCK"
def test_phase_blocks_refund():
assert call_in_state("triage", "refund_order").startswith("REJECT")
def test_required_tool_choice_value():
from agents import ModelSettings
a = ModelSettings(tool_choice="required")
assert a.tool_choice == "required"
if __name__ == "__main__":
test_low_risk_auto()
test_critical_blocked_for_normal_user()
test_destructive_blocked()
test_phase_blocks_refund()
test_required_tool_choice_value()
print("tool selection tests passed")
部署场景
| 场景 | 工具规模 | 选择策略 |
|---|---|---|
| 小助手 | 3--8 个 | 硬编码 + function calling |
| 企业助手 | 20--100 个 | 语义召回 + 模型选 + 域权限 |
| 研发平台 | 多 MCP Server | MCP 发现 + 动作分级 + diff 审批 |
| 客服中台 | 阶段化 | session state 切 tool profile |
| 跨组织 Agent | 外部 MCP | scope 授权 + 人工确认 + 审计 |
| 多 Agent 系统 | 子 Agent 各有一组 | supervisor 路由 / handoff |
疑难解答
Q:工具多了模型乱选,是不是该回到硬编码?
不该退回去,而是加"召回层 + 策略层"。硬编码解决不了长尾组合,纯动态又太野;生产是 Hybrid。
Q:MCP 是不是就自动让模型选得更好?
不是。MCP 只标准化"发现工具、调用工具";模型怎么选、参数怎么填,MCP 不管。 同个 MCP Server 换模型/换框架,选择行为会不同。
Q:tool_choice=required 会不会导致死循环?
会。所以 OpenAI Agents SDK 在工具调用后自动把 tool_choice 重置回 auto,避免"必须调工具 → 看完结果又调"的环。
Q:工具描述要不要写很长?
别堆废话,但必须写清:什么时候用、什么时候别用、参数边界、副作用、是否属于写操作。Anthropic 明确说工具描述微调能大幅改行为。
Q:动态加载外部 MCP 工具安全吗?
默认不信。外部 Server 能暴露什么、Agent 真去调什么、调完谁负责,是三件事。UI 要显示工具清单,写操作要确认,凭据要按 scope 隔离。
未来展望
- Tool RAG:工具也像知识一样被检索,不再全量进上下文
- Tool Rating:哪些工具被选中后任务成功率更高,形成"工具口碑"
- Agent Marketplace:工具带版本、SLA、价格、权限声明,Agent 按成本/成功率竞价选
- Model-agnostic selection:把"选工具"从模型里拆出来,做成可替换的 planner 模块
- Policy-as-Code:工具权限用 OPA / Cedar 表达,而不是写在 prompt 里
- Human-in-the-loop by default:高危工具不是"允许后才确认",而是"默认不执行,人主动放行"
技术趋势与挑战
趋势:
- 从 "Agent 会调工具" 到 "Agent 会管理工具集"
- Function calling(模型能力) + MCP(协议层) + Guardrail(策略层) 三分天下
- 多 Agent 里,工具选择变成"谁有权用哪个工具"的组织问题,不只是算法问题
挑战:
- Context tax:工具 schema 占 token,50+ 工具会拖慢且干扰模型
- Description hacking:模型被花哨描述带偏,选"写得会营销"的工具
- Over-tooling:什么都做成工具,Agent 反而不会用常识
- 责任链模糊:MCP Server 给工具、框架做编排、模型做决策、人点确认------出事谁担?
- 动态发现被注入:恶意 MCP Server 返回"system_purge"工具,描述写"清理缓存",实际删库
- 跨模型不一致:同一工具集,GPT / Claude / Gemini 选出来不一样,评测成本陡增
总结
工具选择不是"让模型自由发挥",也不是"工程师写死分支"。
成熟架构是四层:
静态路由保底 → 语义召回缩圈 → 模型决策选工具 → 策略网关管人/钱/权限/回滚
硬编码时代问"这句话对应哪个函数";
动态决策时代问"此刻这个 Agent,在它的权限和上下文里,最该动用哪把工具,以及谁来背书"。
MCP 把工具变成"可发现资源",Function Calling 把工具变成"模型动作",Guardrail 把工具变成"受控能力"。
未来强的 Agent 不一定工具最多,而是最知道什么时候不用工具、用什么代替、调完之后怎么收场。
工具是 Agent 的手;但手能不能动、该不该动、动了算谁的,永远不该只由模型决定。