给 Agent 装上“手“:Function Calling / Tool Use 从零讲透

📚 系列:《Agent 从入门到上手》· 30 天连载 · 第 5 篇 🎯 本篇目标:彻底搞懂 Function Calling(函数调用) ------工具说明书怎么写、模型怎么选工具、参数怎么填、结果怎么回传,以及那个最反直觉的真相:模型从头到尾没有执行过任何东西 ⏱️ 阅读时长:约 22 分钟


一、开场:被关在玻璃房里的五星大厨

先想象一个有点荒诞的场景。

你花大价钱请来一位五星大厨,米其林三星水平,脑子里装着八千道菜谱,你报出任何一个菜名他都能把步骤、火候、调料精确到克。

但问题是------他被关在一间玻璃房里,没有手,没有灶,没有食材,连盐罐都够不着。

你在外面喊:"师傅,来盘宫保鸡丁!"

他隔着玻璃,声情并茂地给你描述了一遍宫保鸡丁怎么做:

"首先选用鸡腿肉去骨切丁,加料酒生抽腌制十五分钟......油温六成热下干辣椒段炝锅......最后沿锅边淋入兑好的碗芡,大火快速翻炒均匀出锅......"

描述得一字不差。

你听得热泪盈眶,然后低头看了看手里------还是没有宫保鸡丁。

这就是 2023 年之前所有大模型的真实处境:什么都懂,什么都干不了。 它知道 API 该怎么写、SQL 该怎么拼、天气该怎么查,但它无法真的伸手去按那个按钮。

而 Function Calling (函数调用,也叫 Tool Use ,工具使用),就是在玻璃房上开一个窗口,递进去一张工单,再把做好的菜递出来。

🎯 本篇要解决的问题 :模型怎么"知道"有哪些工具可用?它凭什么能把"帮我看看北京热不热"翻译成 query_weather(city="北京")?翻译错了怎么办?工具是不是挂得越多越好?


二、先正名:Function Calling ≠ 模型替你调用函数

这是新手最容易搞反、也最要命的一个误解。

很多人一听"函数调用",脑子里浮现的画面是:模型自己跑去执行了一个函数,拿到结果,然后回答我。

错。大错特错。

真相是:

🔑 模型只会说"我建议调用某某函数、参数某某某",它一步都不会执行。真正动手的,永远是你自己写的那段代码。

比喻:点菜 ≠ 做菜

把模型想成服务员 ,把你自己的代码想成后厨。

  • 你(顾客)说:"来盘宫保鸡丁,微辣。"
  • 服务员(模型)不会自己冲进厨房炒菜 。他做的是:在这张点菜单上,工整地写下 菜名: 宫保鸡丁、辣度: 微辣,然后把单子递到窗口。
  • 后厨(你的代码)看到单子,真的开火、翻炒、装盘。
  • 服务员把菜端回给你,顺便说一句"您慢用"。

服务员从头到尾没碰过锅铲。 但他决定"写下什么菜名、什么辣度"------这个决定,就是 Function Calling 的全部价值。

所以 Function Calling 本质上不是"执行",而是一次结构化输出:让模型不要用人话写作文,而是按你给定的格式,输出一段机器能直接解析的 JSON。

两种输出,对比着看

同样一句"帮我看看北京今天热不热",两种输出方式:

对比项 ❌ 没用 Function Calling(自然语言输出) ✅ 用了 Function Calling(结构化输出)
模型说了什么 "北京今天最高气温 26℃,天气晴朗,体感较为舒适......"(这是它编的) {"name":"query_weather","arguments":{"city":"北京"}}
你的代码能直接解析吗 不能,得写正则、做容错、祈祷它别换个说法 能,json.loads() 一行搞定
数据是真的吗 不知道------模型在"回忆"训练数据里的天气 真的,因为接下来会去真的调天气接口
换个问法会崩吗 会,措辞一变,正则就废了 不会,字段是固定的
出错能自动重试吗 难 容易,校验不过就把错误丢回去让它重填

看明白了吗? Function Calling 一次解决了三个问题:格式稳定 、接得上真实世界 、出错可修。


三、一次完整的 Function Calling,到底转了几步?

先看全景,别急着抠细节。

用一张表把每一步拆开:

步 谁在干 干什么 产出
① 🫵 你的代码 把用户问题 + 全部工具说明书,一起打包发请求 一个 HTTP 请求体
② 🧠 模型 读说明书,判断"这活儿要不要用工具、用哪个" 一个内部决策
③ 🧠 模型 不回人话,回一段结构化 JSON 工单 tool_calls: [{name, arguments}]
④ 🫵 你的代码 真正执行------按 name 找到本地函数,按 arguments 传参跑起来 函数返回值
⑤ 🫵 你的代码 把返回值包装成一条 role="tool" 的消息,追加到对话末尾 新的消息列表
⑥ 🧠 模型 看着结果,生成给用户的最终回复 人话答案

第 ④ 步是分水岭。 前三步模型在"动嘴",第四步开始你的代码在"动手"。

而且注意第 ⑤ 步------结果是要原样塞回对话的。今天先记住这个动作,等我们进入 RAG(Day 09)和多轮记忆(Day 08)时,你会发现全都是这个套路的变体。

工单长什么样(真实返回结构)

模型在第 ③ 步返回的,大致是这个形状:

复制代码
{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "call_9Xk2pQ",
      "type": "function",
      "function": {
        "name": "query_weather",
        "arguments": "{\"city\":\"北京\",\"unit\":\"celsius\"}"
      }
    }
  ]
}

⚠️ 全文第一个必踩的坑,请注意 arguments 那一行:

arguments 是一个 JSON 字符串,不是一个 JSON 对象。 你必须 json.loads() 一次才能拿到字典。很多人直接 args["city"] 然后收到一个"字符串索引必须是整数"的报错,排查半小时。

顺带说一句,不同厂家的字段命名略有出入(有的叫 function.arguments,有的把参数直接给成对象),但**"模型给建议、代码去执行"这个分工是行业通用的**,不会有例外。


四、重头戏:工具说明书到底该怎么写?

模型看不见你的源代码,看不见你的注释,看不见你的数据库。

它唯一能看见的,就是你交给它的那份工具声明。

比喻:这不是代码注释,这是"新员工岗位说明书"

很多程序员写工具声明时,把它当成代码注释来写------随便糊一句"查询工具",反正"我自己知道这是干嘛的"。

问题是:读这份文档的不是一个了解你项目背景的同事,而是一个刚入职三秒钟、没有任何上下文、只能靠字面意思猜的临时工。

你给他写"工具 A",他只能瞎猜。 你给他写"查询指定城市当日天气;当用户问天气/气温/冷热/带伞时调用;不支持多日预报和境外城市",他就能干对。

写工具声明,本质上是在教一个聪明但零背景的人怎么干活。 你不是在写代码,你是在当领导。

一份能用的声明长这样

复制代码
{
  "name": "query_weather",
  "description": "查询指定城市的当日天气,返回天气状况、气温和风力。当用户询问某城市天气、气温、冷热、是否下雨、要不要带伞时调用。不支持:查询多日天气预报、查询境外城市、查询历史天气。",
  "parameters": {
    "type": "object",
    "properties": {
      "city": {
        "type": "string",
        "description": "中国境内城市名称,例如:北京、天津、上海"
      },
      "unit": {
        "type": "string",
        "enum": ["celsius", "fahrenheit"],
        "description": "温度单位,默认 celsius(摄氏度)"
      }
    },
    "required": ["city"]
  }
}

六个字段,逐个说人话

字段 人话翻译 写砸了会怎样
name 工具的身份证号,全库唯一 叫 tool1 → 模型分不清谁是谁
description 岗位说明书,最关键 写"查询工具" → 该用的时候不用,不该用的时候乱用
parameters 一张工单模板(JSON Schema) 缺了 → 模型不知道要填啥
properties 工单上每一栏 只有字段名没描述 → 模型猜你心思
enum 把填空题改成选择题 不枚举 → 收到"开尔文""度"等各种野值
required 必填星号 ⭐ 漏标 → 收到 null,然后全线崩溃

💡 一句话记忆 :name 是它叫什么,description 是它什么时候上场,parameters 是上场要带什么装备,required 是哪些装备不带别出门。

好描述 vs 坏描述,实战对照

场景 ❌ 坏写法 ✅ 好写法
只写功能 "查询天气" "查询指定城市的当日天气。当用户问天气、气温、冷热、要不要带伞时调用。不支持:多日预报、境外城市、历史天气。"
不写边界 "计算工具" "计算数学表达式的值,支持加减乘除与小括号。不支持:解方程、矩阵运算、单位换算。"
参数无说明 {"city": {"type":"string"}} {"city": {"type":"string","description":"中国境内城市名,例如:北京、天津、上海"}}
该枚举不枚举 {"unit":{"type":"string"}} {"unit":{"type":"string","enum":["celsius","fahrenheit"]}}
工具职责含糊 "处理用户请求" "根据订单号查询订单的物流与支付状态。不涉及:修改、取消、退款。"

注意好写法的共同结构,我把它总结成 "三句话公式":

复制代码
干什么(一句话功能)
+ 什么时候用(触发场景,最好带用户原话里的关键词)
+ 不干什么(明确的边界,防止越权调用)

第三句最容易被省略,但它才是防止乱调用的关键。你不写"不支持退款",模型就可能在用户说"我要退款"时,硬着头皮去调查询工具。


五、模型是怎么"选"工具、"填"参数的?

现在揭晓那个黑箱。

选工具:本质是"比谁更像"

模型做的事情,用一个极其粗糙但足够直观的比喻来说,就是拿着用户的话,去和每份说明书"比对相似度":

用户问 与 query_weather 相似度 与 calc 相似度 与 search_order 相似度 选中
北京今天天气怎么样? 0.188 0.000 0.000 query_weather
帮我算一下 (80-68)*12 0.083 0.250 0.000 calc
订单 A1002 到哪了? 0.000 0.100 0.800 search_order

(上表为本篇动手环节实测输出,原理示意)

真实的模型当然不是在做字面匹配------它是在高维语义空间里做匹配,所以你说"魔都今天淋不淋",它也能对上"上海/下雨"。但决策机制的外在表现是一样的:谁的描述和用户意图最贴,选谁。

推论极其重要:既然是靠"说明书的贴合度"来选,那么------

🔑 你把工具描述得越清楚、越有区分度,模型就选得越准。模型的"工具选择能力",八成取决于你那张说明书的质量。

填参数:最容易翻车的一步

选对工具只是上半场。下半场是从用户的话里把参数抠出来。

用户说:"帮我查下北京的温度 " → 模型要自己判断出 city="北京",而且unit 没提,得用默认值。

这一步常见的翻车姿势有四种:

翻车类型 具体表现 举个栗子 🌰 怎么防
🎭 参数幻觉 编造一个不存在的值 unit="开尔文"(枚举里根本没有) enum 约束 + 校验
🕳️ 必填漏填 该填的没填,给了 null 用户没说城市 → city=null required + 校验 + 追问
🔀 类型跑偏 该给数字给了字符串 city=123(把区号当城市名) 类型校验 + 强转
➕ 凭空加戏 说明书里没这参数,它非要传 多传一个 extra="x" 未知参数拒绝

这四种错误,模型一个都保证不了不犯。

所以工程上有一条铁律:

⚠️ 永远不要相信模型吐出来的参数。先校验,再执行。校验不过,把错误原样丢回去让它重填。

这就是 Day 05 最值钱的一句话。后面 Day 12(错误自愈)整篇都在讲怎么把这件事做优雅,但根子在这里。


六、工具是不是挂得越多越好?

终于到了那个经典问题:我给 Agent 挂 50 个工具,它能干 50 种活,岂不是赚翻了?

比喻:钉满 50 把锤子的工具墙

你家墙上钉了 3 把锤子,你能一眼看出哪把是羊角锤、哪把是橡胶锤、哪把是石工锤。

现在墙上钉了 50 把长得差不多的锤子,其中 8 把都叫"锤子",还有 12 把的标签上写着"工具(备用)"。

你去拿一把------你确定你拿对了?

模型面对的就是这面墙。工具越多:

  1. 描述之间越容易撞车(一堆"查询工具""数据工具")
  2. 注意力被稀释(每份说明书分到的"关注度"变少)
  3. 选择的随机性变高(今天选对,明天同一个问题选错)

实测数据(本篇动手环节跑出来的)

我们做了个实验:固定 8 条测试问句,逐步往工具箱里塞干扰工具(描述故意"蹭"关键词,模拟真实系统里工具越堆越像的情况),看"一次选对"的比率:

工具总数 一次选对率 条形图
3 91.9% █████████████████████████
8 81.6% ██████████████████████
20 75.3% █████████████████████
50 71.9% ██████████████████

而同样只有 3 个工具 ,只把说明书从"认真版"换成"糊弄版"(tool_a 描述"查询"、tool_b 描述"计算"、tool_c 描述"查询"------两个一模一样),结果是:

说明书版本 正确率
✅ 认真版(写清干什么 / 何时用 / 不干什么) 8/8 = 100%
❌ 糊弄版("查询""计算""查询") 4/8 = 50%

看出哪个才是致命因素了吗?

工具从 3 个加到 50 个,正确率掉了 20 个百分点 。 说明书从认真改到糊弄,正确率直接腰斩。

🎯 结论:工具数量有代价,但说明书质量才是命门。先把手上 5 个工具的描述写好,再去想第 6 个工具。

工具多了怎么办?三条实用手段

手段 怎么做 适用场景
🗂️ 分组路由 先让一个"总机"Agent 判断属于哪一类,再只把该类的 5-8 个工具塞给执行 Agent 工具超过 ~15 个
🔍 两阶段检索 工具太多时,先用向量检索召回 Top-K 个相关工具,再交给模型挑 工具几十上百个
✂️ 砍掉重叠 描述高度相似的工具,合并或删掉一个 任何时候

顺便说一句,各家对"一次最多挂几个"并没有硬性上限,而且能力一直在涨。上面这张表想表达的不是"50 个一定不行",而是"每多挂一个,你都在为选择准确率付利息"。


七、动手环节:纯标准库搭一个完整的 Function Calling 闭环

光说不练假把式。下面这段代码只用 Python 标准库,把"工具声明 → 模拟大脑选工具 → 填参数 → 参数校验 → 分发执行 → 结果回传 → 自愈重填"整条链路跑通。

说明:为了不依赖任何 API Key,这里用一个关键词匹配的小函数模拟"大脑"。它只能读工具说明书(跟真实模型一样),看不到函数实现。把它换成真实大模型调用,结构完全不变。

完整代码

复制代码
# -*- coding: utf-8 -*-
"""Function Calling 完整闭环 · 纯标准库实现"""
import json, random, re

# ---------- 1. 真实可执行的函数(Agent 的"手") ----------
WEATHER_DB = {"北京": "晴,26℃,微风", "天津": "多云,24℃", "上海": "小雨,22℃"}
ORDER_DB   = {"A1001": "已发货,预计明天送达", "A1002": "待付款"}
TEMP_C     = {"北京": 26, "天津": 24, "上海": 22}
CITIES     = ["北京", "天津", "上海"]

def query_weather(city, unit="celsius"):
    if city not in WEATHER_DB:
        return f"抱歉,暂不支持查询「{city}」的天气"
    if unit == "fahrenheit":
        return WEATHER_DB[city] + f";约 {TEMP_C[city]*9/5+32:.0f}℉"
    return WEATHER_DB[city]

def calc(expression):
    if not re.fullmatch(r"[0-9\.\+\-\*/() ]+", expression or ""):
        return "表达式非法:只允许数字与 + - * / ( )"
    try:
        return str(eval(expression))
    except Exception as e:
        return f"计算失败:{e}"

def search_order(order_id):
    return ORDER_DB.get(order_id, "未找到该订单")

IMPLEMENTATIONS = {"query_weather": query_weather,
                   "calc": calc,
                   "search_order": search_order}

# ---------- 2. 工具声明(模型唯一能看到的东西) ----------
GOOD_TOOLS = [
    {
        "name": "query_weather",
        "description": "查询指定城市的当日天气,返回天气状况、气温和风力。"
                       "当用户询问某城市天气、气温、冷热、是否下雨、要不要带伞时调用。"
                       "不支持:查询多日天气预报、查询境外城市、查询历史天气。",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {"type": "string",
                         "description": "中国境内城市名称,例如:北京、天津、上海"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"],
                         "description": "温度单位,默认 celsius"},
            },
            "required": ["city"],
        },
    },
    {
        "name": "calc",
        "description": "计算一个数学表达式的值,支持加减乘除和小括号。"
                       "当用户要求算账、求值、问「多少」「等于几」、算百分比时调用。"
                       "不支持:解方程、矩阵运算、单位换算。",
        "parameters": {
            "type": "object",
            "properties": {
                "expression": {"type": "string",
                               "description": "待计算的表达式,例如 (80 - 68) * 12"},
            },
            "required": ["expression"],
        },
    },
    {
        "name": "search_order",
        "description": "根据订单号查询订单的物流与支付状态。"
                       "当用户询问快递到哪了、订单发货没有、是否已付款时调用。"
                       "不支持:修改订单、取消订单、查询历史订单。",
        "parameters": {
            "type": "object",
            "properties": {
                "order_id": {"type": "string",
                             "description": "订单号,大写字母加数字,例如 A1001"},
            },
            "required": ["order_id"],
        },
    },
]

# ---------- 3. 模拟大脑:选工具 + 填参数 ----------
def tokenize(text):
    toks = set(re.findall(r"[A-Za-z0-9_]+", text.lower()))
    cn = re.findall(r"[\u4e00-\u9fff]", text)
    for i, ch in enumerate(cn):
        toks.add(ch)
        if i + 1 < len(cn):
            toks.add(ch + cn[i + 1])
    return toks

def brain_select_tool(tools, query, noise=0.0):
    qt = tokenize(query)
    scores = {}
    for t in tools:
        tt = tokenize(t["name"] + " " + t["description"] + " " +
                      " ".join(t["parameters"].get("properties", {})))
        s = len(qt & tt) / max(1, len(qt))
        if noise:
            s += random.gauss(0, noise)      # 模拟注意力分散
        scores[t["name"]] = round(s, 3)
    return max(scores, key=scores.get), scores

CLARIFY = object()   # 哨兵:放弃硬猜,改为向用户追问

def brain_fill_arguments(tool, query, retry=False, sloppy=False):
    props = tool["parameters"]["properties"]
    args = {}
    for name, schema in props.items():
        enum = schema.get("enum")
        if enum:
            if sloppy:                        # 模拟第一遍瞎编一个枚举值
                args[name] = "开尔文"; continue
            hit = [e for e in enum if e in query]
            args[name] = hit[0] if hit else enum[0]
        elif name == "city":
            hit = [c for c in CITIES if c in query]
            args[name] = hit[0] if hit else None
        elif name == "order_id":
            m = re.search(r"[A-Za-z]\d{3,}", query)
            args[name] = m.group(0).upper() if m else None
        elif name == "expression":
            m = re.search(r"[0-9\.\+\-\*/() ]{3,}", query)
            args[name] = m.group(0).strip() if m else None

    if retry:    # 第二遍还抠不出必填项 → 不硬猜,直接问用户
        missing = [r for r in tool["parameters"].get("required", [])
                   if args.get(r) in (None, "")]
        if missing:
            return CLARIFY, missing
    return args, None

# ---------- 4. 参数校验:先验货,再执行 ----------
TYPE_MAP = {"string": str, "integer": int, "number": (int, float),
            "boolean": bool, "array": list, "object": dict}

def validate_arguments(tool, args):
    errors, schema = [], tool["parameters"]
    props = schema.get("properties", {})
    for req in schema.get("required", []):
        if req not in args or args[req] in (None, ""):
            errors.append(f"缺少必填参数:{req}")
    for k, v in (args or {}).items():
        if k not in props:
            errors.append(f"出现说明书里没有的参数:{k}"); continue
        want = props[k].get("type")
        if want == "integer" and isinstance(v, bool):
            errors.append(f"{k} 应为 integer,收到 bool")
        elif TYPE_MAP.get(want) and not isinstance(v, TYPE_MAP[want]):
            errors.append(f"{k} 类型不符:期望 {want},实际 {type(v).__name__}")
        enum = props[k].get("enum")
        if enum and v not in enum:
            errors.append(f"{k} 取值 {v!r} 不在枚举 {enum} 内")
    return errors

# ---------- 5. 分发执行:真正动手的只有这里 ----------
def dispatch(tool_call, tools, implementations):
    name, args = tool_call["name"], tool_call.get("arguments", {}) or {}
    schema = next((t for t in tools if t["name"] == name), None)
    if schema is None:
        return {"ok": False,
                "error": f"工具 {name} 不存在,可用工具:{[t['name'] for t in tools]}"}
    errors = validate_arguments(schema, args)
    if errors:
        return {"ok": False, "error": "参数校验未通过:" + ";".join(errors),
                "retry_hint": True}
    try:
        return {"ok": True, "result": implementations[name](**args)}
    except TypeError as e:
        return {"ok": False, "error": f"参数签名对不上:{e}", "retry_hint": True}
    except Exception as e:
        return {"ok": False, "error": f"执行出错:{type(e).__name__}: {e}"}

# ---------- 6. 主循环:串成一次完整对话 ----------
def run_agent(query, tools, implementations, max_rounds=3, sloppy=False):
    print("=" * 62); print(f"🙋 用户:{query}"); print("=" * 62)
    for r in range(1, max_rounds + 1):
        name, scores = brain_select_tool(tools, query)
        print(f"\n🧠 第 {r} 轮 · Thought:这活儿该用「{name}」")
        print("   候选得分:" + "  ".join(
            f"{k}={v}" for k, v in sorted(scores.items(), key=lambda x: -x[1])[:3]))
        args, missing = brain_fill_arguments(
            next(t for t in tools if t["name"] == name),
            query, retry=r > 1, sloppy=(sloppy and r == 1))
        if args is CLARIFY:
            print(f"🙋 Action:向用户追问 ------ 还缺 {missing},我不瞎猜")
            print("🗣️  最终回复:请问您想查询哪个城市的天气?支持:北京/天津/上海\n")
            return
        print(f"✍️  Action: {name}({', '.join(f'{k}={v!r}' for k, v in args.items())})")
        out = dispatch({"name": name, "arguments": args}, tools, implementations)
        if not out["ok"]:
            print(f"👀 Observation:{out['error']}")
            if out.get("retry_hint") and r < max_rounds:
                print("↩️  把错误原样丢回给大脑,让它重新填一次"); continue
            return out
        print(f"👀 Observation:{out['result']}")
        print(f"\n🗣️  最终回复:{out['result']}\n")
        return out

if __name__ == "__main__":
    run_agent("北京今天天气怎么样?", GOOD_TOOLS, IMPLEMENTATIONS)
    run_agent("帮我算一下 (80 - 68) * 12", GOOD_TOOLS, IMPLEMENTATIONS)
    run_agent("订单 A1002 到哪了?", GOOD_TOOLS, IMPLEMENTATIONS)
    run_agent("帮我查下北京的温度,输出用 fahrenheit",
              GOOD_TOOLS, IMPLEMENTATIONS, sloppy=True)   # 演示参数幻觉→自愈
    run_agent("帮我查一下天气", GOOD_TOOLS, IMPLEMENTATIONS)   # 演示追问

实测输出(真跑出来的,不是编的)

① 三个正常问句,一次选对:

复制代码
==============================================================
🙋 用户:北京今天天气怎么样?
==============================================================

🧠 第 1 轮 · Thought:这活儿该用「query_weather」
   候选得分:query_weather=0.188  calc=0.0  search_order=0.0
✍️  Action: query_weather(city='北京', unit='celsius')
👀 Observation:晴,26℃,微风,紫外线强

🗣️  最终回复:晴,26℃,微风,紫外线强

==============================================================
🙋 用户:帮我算一下 (80 - 68) * 12
==============================================================

🧠 第 1 轮 · Thought:这活儿该用「calc」
   候选得分:calc=0.25  query_weather=0.083  search_order=0.0
✍️  Action: calc(expression='(80 - 68) * 12')
👀 Observation:144

🗣️  最终回复:144

==============================================================
🙋 用户:订单 A1002 到哪了?
==============================================================

🧠 第 1 轮 · Thought:这活儿该用「search_order」
   候选得分:search_order=0.8  calc=0.1  query_weather=0.0
✍️  Action: search_order(order_id='A1002')
👀 Observation:待付款,请在 2 小时内完成支付

🗣️  最终回复:待付款,请在 2 小时内完成支付

② 参数校验拦住四种翻车:

复制代码
工单 {"name": "query_weather", "arguments": {}}
  → ❌ 参数校验未通过:缺少必填参数:city

工单 {"name": "query_weather", "arguments": {"city": "北京", "unit": "开尔文"}}
  → ❌ 参数校验未通过:unit 取值 '开尔文' 不在枚举 ['celsius', 'fahrenheit'] 内

工单 {"name": "query_weather", "arguments": {"city": 123}}
  → ❌ 参数校验未通过:city 类型不符:期望 string,实际 int

工单 {"name": "search_order", "arguments": {"order_id": "A1001", "extra": "x"}}
  → ❌ 参数校验未通过:出现说明书里没有的参数:extra

工单 {"name": "calc", "arguments": {"expression": "__import__('os').system('echo pwned')"}}
  → ✅ 表达式非法:只允许数字与 + - * / ( )

工单 {"name": "non_exist_tool", "arguments": {}}
  → ❌ 工具 non_exist_tool 不存在,可用工具:['query_weather', 'calc', 'search_order']

③ 自愈:第一遍填错,第二遍改回来:

复制代码
🙋 用户:帮我查下北京的温度,输出用 fahrenheit

🧠 第 1 轮 · Thought:这活儿该用「query_weather」
✍️  Action: query_weather(city='北京', unit='开尔文')
👀 Observation:参数校验未通过:unit 取值 '开尔文' 不在枚举 [...] 内
↩️  把错误原样丢回给大脑,让它重新填一次

🧠 第 2 轮 · Thought:这活儿该用「query_weather」
✍️  Action: query_weather(city='北京', unit='fahrenheit')
👀 Observation:晴,26℃,微风,紫外线强;换算成华氏度约 79℉

🗣️  最终回复:晴,26℃,微风,紫外线强;换算成华氏度约 79℉

④ 抠不出必填项 → 不硬猜,转而追问:

复制代码
🙋 用户:帮我查一下天气

🧠 第 1 轮 · Thought:这活儿该用「query_weather」
✍️  Action: query_weather(city=None, unit='celsius')
👀 Observation:参数校验未通过:缺少必填参数:city;city 类型不符:期望 string,实际 NoneType
↩️  把错误原样丢回给大脑,让它重新填一次

🧠 第 2 轮 · Thought:这活儿该用「query_weather」
🙋 Action:向用户追问 ------ 还缺 ['city'],我不瞎猜
🗣️  最终回复:请问您想查询哪个城市的天气?目前支持:北京 / 天津 / 上海

🧯 一个严肃提醒 :demo 里为了演示用了 eval(),并且靠字符白名单挡住了危险输入。生产环境请直接用正经的表达式解析库或沙箱执行 ,别拿 eval 开玩笑------模型吐出来的东西,永远当成不可信输入处理。


八、本篇小结(建议截图保存)

  1. 🧑‍🍳 Function Calling 解决的是"有脑子没手"------它让模型从"用人话描述怎么做"变成"按格式递一张工单"。
  2. 🙅 模型从不执行任何函数。 它只输出"我建议调用 X(参数)",真正动手的永远是你自己的代码。这是全文最需要刻进脑子里的一条。
  3. 📋 工具声明就是模型能看到的一切 ------它看不见你的源码、注释和数据库。description 是其中最重要的字段。
  4. ✍️ 描述用"三句话公式":干什么 + 什么时候用 + 不干什么。第三句(边界)最容易被省,却最能防止乱调用。
  5. 🎯 能枚举就枚举 (enum),把填空题改成选择题;该必填就标 required(⭐),别指望模型自觉。
  6. 🛡️ 永远先验参再执行。 四类翻车(幻觉 / 漏填 / 类型错 / 凭空加参数)模型一个都避免不了,靠代码兜底。
  7. ↩️ 校验失败不要直接报错给用户 ------把错误原样丢回对话,让模型自己重填;实在抠不出来就追问用户,比瞎猜强一百倍。
  8. 🔧 工具不是越多越好。 实测:3→50 个工具,选对率从 91.9% 掉到 71.9%;而说明书从认真改成糊弄,正确率从 100% 腰斩到 50%。先把 5 个工具描述写好,再想第 6 个。
  9. 🔌 把 demo 里的 brain_select_tool 换成真实大模型调用,其余结构一行都不用改------这就是 Function Calling 的标准骨架。

📝 课后作业(认真做,下一篇直接用)

  1. 改描述题 :下面这份工具声明会让 Agent 频繁误调用,请指出至少 3 个问题并重写它。 json {"name": "handle", "description": "处理订单相关的事情", "parameters": {"type": "object", "properties": {"id": {"type": "string"}}}}
  2. 加护栏题 :给动手环节的 validate_arguments() 补两条规则------① 字符串长度上限 100;② city 必须是 CITIES 名录内的值。补齐后重跑,看"帮我查一下深圳天气"会输出什么。
  3. 思考题:你的 Agent 挂了 30 个工具,用户问"帮我把上周的报销单汇总发我邮箱"。请用本篇的"分组路由"思路,设计一个两阶段方案,说明每个阶段各暴露哪些工具、为什么。

🔮 下集预告

Day 06|别再被名词绕晕了:Agent / Workflow / RPA / Copilot 到底啥关系

到今天为止,我们已经把"一只 Agent 是怎么构成的、怎么转起来的、怎么动手的"全讲完了。

但你大概率已经被朋友圈的名词轰炸得头晕了:Agent、Workflow(工作流)、RPA(机器人流程自动化)、Copilot(副驾驶)------这四个词被用得极其混乱。有人说 RPA 就是 Agent 的祖宗,有人说 Workflow 早晚被 Agent 干掉,还有人把任何带 AI 两个字的东西都叫 Copilot。

明天我们一次性把这事儿说清楚:

  • 🗺️ 一张图讲清四个概念------它们真正的区别只有一个维度
  • ⚖️ 什么时候该用工作流,而不是 Agent ------答案可能出乎你的意料:大部分场景根本不需要 Agent
  • 🌳 一张判断决策树------拿到需求,照着走就能选出方案

顺便剧透一句:明天这篇,可能会帮你省下一笔本来要打水漂的开发预算。 毕竟,用 Agent 去干本该用 if-else 干的活,是这个行业目前最流行的一种浪费。明天 12:00 见。😉


🗺️ 系列导航

阶段 篇目 主题
认知篇 Day 01-06 搞清楚 Agent 是个啥(📍 你在这里:Day 05)
能力篇 Day 07-14 提示词、记忆、RAG、规划、自愈、上下文工程
框架篇 Day 15-21 LangChain / LangGraph / LlamaIndex / Dify / 手写 mini Agent
多智能体篇 Day 22-27 多智能体架构、MCP、A2A、人机协同
工程化篇 Day 28-33 评测、可观测、成本优化、安全护栏、生产落地、收官实战

已更新 :Day 01|Agent 到底是什么? · Day 02|LLM 和 Agent 的区别 · Day 03|解剖一只 Agent:四大件 · Day 04|ReAct 循环:想→做→看→再想

每天 12:00 更新一篇。 觉得有用的话,点个收藏,别到时候找不到路了 😄 有问题欢迎在评论区留言,每一条我都会看,典型问题会写进后续的加餐篇。

相关推荐
MgArcher1 小时前
本地部署一个 7B 大模型,到底需要多大显存?
人工智能
姜楠聊AI转型1 小时前
团队AI编程落地第 8 篇:AI 编程 Rules 规则模板(可直接复制):分层写法 + 5 个原则 + 维护机制
人工智能·ai编程·研发管理·vibe coding·ai coding·coding agent
AliCloudROS1 小时前
从 TPOT 188ms 到 22ms:大模型多机分布式部署的并行策略实测
人工智能
桃西西呀1 小时前
LangGraph4j之七:子图与组合
人工智能·llm·ai编程
待遇安然风1 小时前
DeepSeek 架构研读 (二):DeepSeek-V2 MoE 架构与 MLA 机制详解
人工智能
预知同行1 小时前
为什么大家都在劝退 Agent 开发?因为多数场景根本轮不到它出场
人工智能
星云低代码开发平台1 小时前
AI 销售周报的数据契约:CRM 商机、ERP 订单与异常核对
大数据·人工智能·企业ai
YH_Analytics1 小时前
2026-2032 蘑菇罐头市场规模与增长预测
人工智能
星云低代码开发平台1 小时前
企业 AI 连接架构:iPaaS、网关、RPA、MCP 与连接器边界
人工智能·架构·rpa·企业ai