吃透Tool Use!AI Agent工具体系、安全与编排全解析
文章目录
P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 传送门http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。
前言
不知道大家有没有想过,普通聊天机器人和真正能干活的AI Agent,差距到底在哪?
说白了就是会不会用工具。大模型脑子里装的全是训练截止时间之前的知识,就跟过年回老家的我一样,消息全是过时的。
你问它今天股票行情,它两眼一抹黑;你让它操作数据库,它只会纸上谈兵。一旦加上Tool Use工具调用,直接原地变身行动派。
网上很多框架把工具调用封装得花里胡哨,面试的时候面试官让手写,当场直接懵圈。今天我就把这块掰开揉碎聊清楚。
1 Agent工具体系,先搞懂工具到底分哪些
工具不是随便写个函数丢给大模型就完事,分类没搞明白,后面踩坑踩得你怀疑人生。
就像你给员工配设备,不能一股脑把公司所有机器全塞给他,不然他都不知道该点哪个按钮。
1.1 按功能用途划分工具
- 信息检索类:网页搜索、知识库查询,纯只读,不会改任何数据,属于安全系数最高的工具。相当于AI的百度。
- 数据操作类:SQL查询、增删改查接口,读写都能干,搞不好就能把表删了,风险拉满。
- 代码执行类:Python沙箱、Shell脚本,威力巨大,用不好直接原地炸环境。
- 通信通知类:发邮件、Webhook推送,负责对外发消息。
- 文件操作、浏览器自动化、第三方服务对接、硬件感知设备等等。
这里有个很现实的趣事,很多新手写Agent,一股脑堆三十多个工具给大模型。
好家伙,大模型直接选择困难症犯了,一会点这个一会点那个,任务没干完,API账单先爆炸。
1.2 按调用模式区分
确定性工具,输入固定输出就固定,比如简单计算器,1+1永远等于2,不会今天等于2明天等于3。
非确定性工具就刺激多了,搜索引擎、查天气,同样的查询,隔一分钟返回结果就不一样。
还有有状态工具,典型就是浏览器自动化,你得先打开网页,才能截图;上次操作会影响下一次。无状态工具每次调用互相独立,互不打扰。
1.3 工具的三大能力边界
很多人做工具只关心功能能不能跑,完全忽略边界约束,线上出bug的时候哭都来不及。
- 功能边界:搜索最多返回几条?支持什么语言?不搞清楚,AI要20条数据,工具只能返回5条,任务直接卡壳。
- 性能边界:超时时间、QPS限制。要是一个数据库查询要30秒才返回,整个Agent流程直接卡死,用户以为程序崩了。
- 安全边界:哪些工具只能读,哪些可以写。写操作必须严格看管,不能放开了随便跑。
1.4 设计工具体系4条黄金原则
这几条都是踩坑踩出来的血泪经验。
第一,最小暴露原则。只给完成任务必须的工具。工具数量一旦超过20个,大模型选错概率蹭蹭往上涨。就跟你外卖APP菜单一百个菜品,选外卖选半小时。
第二,正交性原则。工具功能不要重叠。搞两个搜索工具,大模型一会调A一会调B,两份结果回来,逻辑直接乱套。
第三,可组合性。每个工具只干一件小事,不要搞超级大工具。不要写一个“搜索并且下单”,拆成搜索、下单两个独立工具,调试的时候你会来感谢我。
第四,幂等性。写操作尽量做成幂等。网络抖动重试调用,不会重复创建垃圾数据。不然重试三次,给用户发三封一模一样邮件,用户都以为收到垃圾轰炸。
2 Tool Use完整调用流程,9步链路看懂整个闭环
千万不要误以为工具调用就是“大模型调用函数,拿到结果结束”。
真实工程链路要复杂得多,少其中任意一步,生产环境就等着你半夜起来接告警。
2.1 完整流程链路
用户输入
↓
1 LLM推理理解用户意图
↓
2 工具选择,匹配可用工具
├─不需要工具 → 直接输出文本回复
↓
3 参数生成,输出JSON调用参数
↓
4 参数校验 JSON Schema验证
├─校验失败 → 返回错误,交给大模型重新生成
↓
5 权限检查
├─权限不足 → 拒绝执行返回原因
↓
6 真正执行工具函数或者API
↓
7 结果处理,截断、过滤、脱敏
↓
8 把工具结果注入对话上下文
↓
9 LLM二次推理,整合工具返回内容
↓
输出最终回答给到用户
我见过不少demo直接跳过4、5、7步骤,本地跑demo丝滑无比,一上生产疯狂报错。
大模型生成JSON是会翻车的!它不是永远输出完美格式。你不做参数校验,拿到畸形JSON直接去调用接口,服务直接500。
2.2 Python伪代码实现完整引擎骨架
import json
from typing import Any, Optional
class ToolCallEngine:
def __init__(self, llm_client, tools: list, max_retries: int = 3):
self.llm = llm_client
self.tools = {t["name"]: t for t in tools}
self.max_retries = max_retries
def run(self, user_message: str, context: list) -> str:
messages = context + [{"role": "user", "content": user_message}]
tool_schemas = [t["schema"] for t in self.tools.values()]
for _ in range(self.max_retries):
response = self.llm.chat(
messages=messages,
tools=tool_schemas
)
if not response.tool_calls:
return response.content
messages.append(response)
for call in response.tool_calls:
result = self._execute_tool(call)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result, ensure_ascii=False)
})
return "达到最大重试次数,未能完成任务。"
def _execute_tool(self, call) -> Any:
tool = self.tools.get(call.name)
if not tool:
return {"error": f"未知工具: {call.name}"}
try:
params = json.loads(call.arguments)
except json.JSONDecodeError:
return {"error": "参数格式错误"}
validated = self._validate(params, tool["schema"])
if not validated["ok"]:
return {"error": validated["msg"]}
try:
result = tool["function"](**params)
return self._process_result(result, tool)
except Exception as e:
return {"error": f"执行失败: {str(e)}"}
注意max_retries这个参数,一定要加。
不给最大重试限制,极端情况下会出现无限循环调用工具。就像人陷入死胡同,原地反复转圈,token疯狂烧钱。
2.3 工具结果注入的几种格式
工具返回数据怎么塞回消息列表,很多人不在意,但是会直接影响大模型理解效果。
- JSON:适合结构化数据,嵌套不要超过3层,太深大模型读得头疼。
- Markdown:搜索摘要、长文本内容,列表表格大模型理解很舒服。
- 摘要+详情分层:数据量巨大的时候,先告诉大模型一共有多少条,再放详情,大模型可以判断要不要继续拿更多数据。
3 工具描述工程:写好工具描述,成功率直接翻倍
划重点!大模型看不见你写的工具源代码!!
它判断要不要调用工具,100%依靠你写的描述。描述写得烂,再好的工具代码也是摆设。
很多同学写工具描述就五个字:查询天气。完事。
大模型看了一脸懵,查哪个城市?能不能查历史?参数传啥?出错概率直接拉满。
3.1 一份合格工具描述包含要素
- 名称:动词+名词,见名知意,别搞模糊名字叫process,谁知道是处理图片还是处理付款。
- 描述:干什么,什么时候该用,什么时候不该用。负面说明非常关键。
- 参数JSON Schema:类型、必填、示例、枚举全部写清楚。
- 返回值说明:告诉大模型返回的数据长什么样。
3.2 天气查询工具完整示例
{
"name": "get_weather",
"description": "查询指定城市的实时天气信息,包括温度、湿度、风速和天气状况。适用于用户询问天气、出行建议或需要天气数据辅助决策的场景。不支持查询历史天气数据,历史天气请使用 get_historical_weather 工具。一次只能查询一个城市,一次查询返回当前时刻的天气快照,不包含预报信息。",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,支持中英文。例如:'北京'、'Shanghai'、'New York'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位,默认为 celsius",
"default": "celsius"
},
"include_forecast": {
"type": "boolean",
"description": "是否包含未来 24 小时的简要预报",
"default": false
}
},
"required": ["city"]
}
}
看到没有,里面特意写清楚不支持历史天气。这个就是负面描述。
现实开发里踩过这个坑,没写限制,用户问去年的天气,大模型疯狂调用这个实时接口,次次返回无效数据,循环几十轮。账单哗哗涨。
3.3 常见踩坑案例
| 问题 | 反面例子 | 改进方案 |
|---|---|---|
| 描述太短信息不足 | “搜索工具” | “搜索互联网获取实时信息,支持网页、新闻、图片搜索” |
| 缺少限制条件 | “发送邮件” | “发送邮件,单次最多10个收件人,附件不超过25MB” |
| 参数没有说明 | “q”:{“type”:“string”} | 补充描述搜索关键词,限制字符长度,附带示例 |
写工具描述,是写给大模型看的,不是写给程序员看API文档。重点讲场景,不要讲底层实现。
4 Token预算管理,工具返回太长该怎么办
就算模型支持128K上下文,你也不能一股脑把工具返回全部塞进去。
想象一个场景,调用搜索返回50条结果,一万多token直接怼进上下文。
坏处一大堆:花钱、延迟变高,还有大模型中间遗忘。长上下文中间的内容,它很容易直接看不见。就像看书只看开头结尾,中间全部跳过。
4.1 上下文预算分配经验参考(128K窗口)
系统提示词 + 工具描述: ~8,000 tokens (6%)
对话历史: ~16,000 tokens (12%)
工具返回数据: ~32,000 tokens (25%)
LLM 推理空间: ~72,000 tokens (57%)
总计: ~128,000 tokens
工具返回尽量控制在总上下文四分之一左右,不要贪多。
4.2 四种处理超长返回策略
- 固定截断:直接拿前N条,实现最简单。前提是返回结果已经按相关性排好序。缺点会丢关键信息。
- 字段过滤:删掉没用字段,比如多余URL、HTML标签,只保留title、摘要这些真正需要的字段。性价比很高。
- 摘要压缩:拿小模型先做摘要,把摘要塞上下文。拿算力换token成本。
- 分页加载:先返回第一页,大模型判断不够用,再主动请求下一页。模拟人翻网页。
def process_tool_result(result: dict, token_budget: int) -> dict:
estimated_tokens = estimate_tokens(json.dumps(result))
if estimated_tokens <= token_budget:
return result
# 策略1: 字段过滤
essential_fields = ["title", "summary", "published_at"]
filtered = {k: v for k, v in result.items() if k in essential_fields}
if estimate_tokens(json.dumps(filtered)) <= token_budget:
return filtered
# 策略2: 截断列表数据
if isinstance(result.get("items"), list):
items = result["items"]
while items and estimate_tokens(json.dumps(filtered)) > token_budget:
items = items[:-1]
filtered["items"] = items
filtered["truncated"] = True
filtered["total_count"] = len(result["items"])
return filtered
# 策略3: 摘要压缩
return summarize_result(result, token_budget)
另外数据结构本身也会影响token消耗。能用JSON就别上XML,枚举代替长字符串,公共信息提取出来不要每条重复写一遍。
5 工具选择冲突,工具多了就打架怎么解决
工具一多,新问题来了,大模型选择困难。就跟同时给你两个几乎一模一样的按钮,你也会犹豫按哪个。
5.1 冲突常见来源
第一种,功能重叠。同时放bing搜索、google搜索两个工具。用户要搜信息,它一会调用A一会调用B,甚至两个同时调用,双倍扣费。
第二种,多意图。用户说查天气,然后发邮件通知同事。天气结果是邮件的输入,必须串行。大模型脑子一热给你并行调用,邮件内容是空的直接发送出去,同事收到空白邮件一脸问号。
第三种,工具描述写得模棱两可,两个工具描述分不清边界,大模型彻底懵圈。
5.2 解决冲突手段
- 工具合并:功能高度重合直接合成一个,用参数区分底层引擎。
- 优先级标注:工具描述写明优先场景,优先用哪个,什么场景切换另一个。
- 意图路由:不要一股脑丢全部工具。先用轻量分类,只把相关工具给到模型。一百选一变成五选一,准确率暴涨。
5.3 动态工具发现
静态注册就是启动的时候把所有工具全部加载好。工具一多弊端很大。
动态工具发现,就是按需加载。任务需要什么工具,再从工具注册中心拿对应的工具,不会把全部工具堆在上下文。就像家里的工具箱,需要螺丝刀的时候再拿出来,不要把所有扳手锤子全部摊在桌面上。
class DynamicToolRegistry:
def __init__(self):
self._tools = {}
def register(self, tool: dict):
self._tools[tool["name"]] = tool
def discover(self, task_description: str, top_k: int = 5) -> list:
scored = []
for name, tool in self._tools.items():
score = self._relevance_score(task_description, tool)
scored.append((name, score))
scored.sort(key=lambda x: x[1], reverse=True)
selected = scored[:top_k]
return [self._tools[name] for name, _ in selected]
def _relevance_score(self, task: str, tool: dict) -> float:
task_words = set(task.lower().split())
desc_words = set(tool["description"].lower().split())
overlap = len(task_words & desc_words)
return overlap / (len(task_words) + 1)
上面是简单关键词版本,生产环境可以换成Embedding语义检索匹配。工具数量超过50个,直接上分层工具架构。先做意图分类,只暴露该分类下面的工具集合。
6 Code Interpreter,动态代码执行的特殊工具
普通工具,函数是我们写好,注册给大模型,大模型只填参数。可控,安全,但是不够灵活。
Code Interpreter完全反过来,大模型现场写Python代码,丢到沙箱运行。
好比普通工具是预制菜,热一热直接吃;Code Interpreter是让AI当场买菜炒菜,上限极高,风险也巨大。
6.1 两者简单对比
- 普通预定义工具:可控性高,安全,延迟低,适合标准化重复任务,但是能力被写死。
- 代码解释器:灵活性拉满,可以处理没预想到的数据分析、图表、复杂运算;但是要做沙箱隔离,延迟更高,调试麻烦。
很多人误以为Code Interpreter可以替换全部工具。千万别这么想。高频简单操作,直接写预定义工具,又快又稳。Code Interpreter用来处理那些根本预想不到的探索式任务。
6.2 Code Interpreter完整工作链路
用户请求(例如分析CSV画柱状图)
↓
1 LLM生成Python代码
↓
2 代码安全扫描,拦截高危操作
↓
3 准备隔离沙箱环境
↓
4 设置超时资源限制执行代码
↓
5 收集stdout、stderr、生成图片文件
↓
6 执行结果注入上下文
↓
7 LLM结合输出整理自然语言回答
6.3 沙箱环境,安全才是第一位
这块千万不要本地直接exec执行代码,相当于把服务器大门敞开给黑客随便逛。
合格沙箱必须做到:文件系统隔离、网络隔离、CPU内存时间资源限制、进程降权。
import subprocess
import tempfile
import os
class CodeSandbox:
def __init__(self, timeout: int = 30, memory_limit: str = "512m"):
self.timeout = timeout
self.memory_limit = memory_limit
def execute(self, code: str, files: dict = None) -> dict:
if not self._safety_check(code):
return {"error": "代码包含不安全操作"}
with tempfile.TemporaryDirectory() as workdir:
if files:
for name, content in files.items():
path = os.path.join(workdir, name)
with open(path, "w") as f:
f.write(content)
script_path = os.path.join(workdir, "main.py")
with open(script_path, "w") as f:
f.write(code)
try:
result = subprocess.run(
["docker", "run", "--rm",
"--memory", self.memory_limit,
"--cpus", "1.0",
"--network", "none",
"--read-only",
"-v", f"{workdir}:/workspace:ro",
"-w", "/workspace",
"python:3.11-slim",
"python", "main.py"],
capture_output=True,
text=True,
timeout=self.timeout
)
return {
"stdout": result.stdout[:5000],
"stderr": result.stderr[:2000],
"exit_code": result.returncode
}
except subprocess.TimeoutExpired:
return {"error": "代码执行超时"}
def _safety_check(self, code: str) -> bool:
forbidden = ["os.system", "subprocess", "eval(",
"exec(", "__import__", "open('/"]
return not any(p in code for p in forbidden)
7 工具调用整套安全控制体系
给大模型配上工具,相当于给AI一双手。手能干好事,也能干坏事。
网上很多Agent Demo玩得很溜,一上生产就漏洞百出。提示注入、越权操作、链式攻击,坑多到数不完。
7.1 主要威胁梳理
- 提示注入:网页、文件返回内容里面藏恶意指令,大模型读完工具返回文本,被里面的指令带跑偏,偷偷调用高危工具。这是最难防御的。
- 权限提升:用户只有读权限,大模型生成删除、修改的参数,没有拦截直接执行。
- 敏感数据泄露:工具拿到密码、手机号,跟着上下文到处传递泄露出去。
- 链式攻击:每一步单独调用看着都合法,连在一起搞破坏。先查管理员信息,再发钓鱼邮件。
7.2 权限分级L0‑L4
- L0自由调用:只读工具,搜索、查天气,直接跑不用确认。
- L1用户授权:读取个人日历通讯录,首次要用户授权确认。
- L2确认执行:发邮件发短信,每次执行要人确认。
- L3严格审批:数据库修改、文件变更,记录审计日志,明确确认。
- L4禁止操作:删系统文件、转账,默认直接禁止。
7.3 防御手段
工具返回的数据,明确标记它只是数据,不是指令。可以用特殊标签包裹返回内容,提醒大模型忽略里面指令。
高危操作一定要上“人在环中”Human‑in‑the‑Loop,需要真人确认才往下执行。不要完全信任大模型的判断。
完整审计日志必不可少,每一次工具调用全部落库记录。遇到删除修改业务,优先软删除,保留回滚机会。
8 工具编排,多工具怎么协同干活
简单任务调用单个工具就搞定,复杂任务要一连串工具互相配合。这就是工具编排。三大基础模式:串行、并行、条件分支。复杂任务组合这三种模式,用DAG有向无环图描述依赖。
8.1 串行编排
上一步输出作为下一步输入,一步接一步执行。比如查完天气,拿天气结果写邮件再发送。逻辑简单好调试,但是总耗时累加。
def serial_orchestration(user_request: str, tools: list):
"""串行工具编排:前一个工具的输出作为后一个的输入"""
current_input = user_request
results = []
for tool in tools:
# LLM 根据当前输入和工具描述生成调用参数
params = llm_generate_params(current_input, tool)
result = tool.execute(**params)
results.append({"tool": tool.name, "result": result})
# 将工具输出转化为下一步的输入
current_input = f"上一步结果: {result}\n请基于此结果继续。"
return results
8.2 并行编排
多个工具互不依赖,同时跑。比如同时查机票、酒店。大幅节省时间,但是要处理单个工具失败,结果聚合的逻辑。
import asyncio
async def parallel_orchestration(user_request: str, tools: list):
"""并行工具编排:所有工具同时执行"""
async def call_tool(tool):
params = llm_generate_params(user_request, tool)
result = await tool.async_execute(**params)
return {"tool": tool.name, "result": result}
tasks = [call_tool(tool) for tool in tools]
results = await asyncio.gather(*tasks)
return results
8.3 条件分支编排
根据上一步返回结果,动态选择接下来走哪条路径。比如查询库存,有货就下单;没货就给用户推替代品。
def conditional_orchestration(user_request: str, plan: dict):
"""条件分支编排:根据中间结果选择不同路径"""
state = {"input": user_request, "results": {}}
for step in plan["steps"]:
if "condition" in step:
condition_met = evaluate_condition(
step["condition"], state["results"]
)
if not condition_met:
continue
tool = get_tool(step["tool"])
params = llm_generate_params(state["input"], tool)
result = tool.execute(**params)
state["results"][step["name"]] = result
if "branch" in step:
branch_key = result.get(step["branch_key"])
next_step = step["branch"][branch_key]
state["input"] = f"基于 {step['name']} 的结果: {result}"
state["results"][step["name"]] = result
8.4 动态编排 vs 静态编排
写死DAG、配置文件是静态编排。行为可控好调试,但是任务灵活度差。
动态编排代表ReAct、Plan‑and‑Execute。大模型运行的时候自己思考下一步调用什么,自己规划任务。更聪明,但容易出现循环,成本高,不好排查故障。
8.5 编排层错误处理要点
区分暂时性错误和永久性错误。网络超时可以重试;参数格式错误重试没用,要修正。
增加熔断机制,某个工具连续报错,暂时不再调用。设置整体任务总超时,不要无限等待。
最后小结
Tool Use不是简单调用几个函数,它是整套完整工程体系。
从工具设计、调用全链路、工具描述、Token控制、冲突处理、代码沙箱、安全权限、多工具编排,每一环都不能偷懒。
很多同学只看demo,以为导入个框架写两行就搞定Agent。等到线上踩坑,才发现坑藏在每一个细节里面。
面试的时候如果让手写Tool Use,不要直接复制框架API,把这整套流程理解清楚,写出来骨架,面试官直接眼前一亮。
P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 传送门http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)