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的时候哭都来不及。

  1. 功能边界:搜索最多返回几条?支持什么语言?不搞清楚,AI要20条数据,工具只能返回5条,任务直接卡壳。
  2. 性能边界:超时时间、QPS限制。要是一个数据库查询要30秒才返回,整个Agent流程直接卡死,用户以为程序崩了。
  3. 安全边界:哪些工具只能读,哪些可以写。写操作必须严格看管,不能放开了随便跑。

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 一份合格工具描述包含要素

  1. 名称:动词+名词,见名知意,别搞模糊名字叫process,谁知道是处理图片还是处理付款。
  2. 描述:干什么,什么时候该用,什么时候不该用。负面说明非常关键。
  3. 参数JSON Schema:类型、必填、示例、枚举全部写清楚。
  4. 返回值说明:告诉大模型返回的数据长什么样。

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 四种处理超长返回策略

  1. 固定截断:直接拿前N条,实现最简单。前提是返回结果已经按相关性排好序。缺点会丢关键信息。
  2. 字段过滤:删掉没用字段,比如多余URL、HTML标签,只保留title、摘要这些真正需要的字段。性价比很高。
  3. 摘要压缩:拿小模型先做摘要,把摘要塞上下文。拿算力换token成本。
  4. 分页加载:先返回第一页,大模型判断不够用,再主动请求下一页。模拟人翻网页。
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 主要威胁梳理

  1. 提示注入:网页、文件返回内容里面藏恶意指令,大模型读完工具返回文本,被里面的指令带跑偏,偷偷调用高危工具。这是最难防御的。
  2. 权限提升:用户只有读权限,大模型生成删除、修改的参数,没有拦截直接执行。
  3. 敏感数据泄露:工具拿到密码、手机号,跟着上下文到处传递泄露出去。
  4. 链式攻击:每一步单独调用看着都合法,连在一起搞破坏。先查管理员信息,再发钓鱼邮件。

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的朋友,否则看看零散的博文就够了。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐