给 Agent 装上「手」——工具调用入门(Function Calling 从猜到查)

25 阅读14分钟

系列:AgentScope 2.0 学习笔记 · 第 004 篇

难度:进阶

适合谁:正在做客服/导购/信息查询类 Agent 的开发者,被「AI 编造答案」困扰的人

前置:建议先读 003《给 Agent 一张「人设卡」》

阅读收获:理解 Function Calling,让 Agent 从「只会说」进化到「会动手」

运行环境:WSL Ubuntu-24.04 + Python 3.11+ + AgentScope 2.x


你有没有见过这种 AI 客服:你问它「玻尿酸保湿霜多少钱」,它张口就来「328 元」,语气笃定、数字精确,你差点就信了。结果点进商品页一看——268 元。它把价格编错了,而且编得跟真的一样。

这不是偶发事故,这是所有「没接数据的 Agent」的通病。大模型天生是个「接话王」,不是「查账本」的;你问它不知道的事,它会顺着你的话头编一个最像样的答案。对客服场景来说,这轻则流失客户,重则引发纠纷。这篇就来治这个病:给 Agent 装上一只手,让它学会「查」,而不是「猜」。


一、Agent 的困境:只会说,不会做

前三篇,我们让 Agent 学会了三件事:编排(001)、记忆(002)、人设(003)。但有一个致命短板一直没解决——它只会「说」,不会「做」。

你问它「玻尿酸保湿霜多少钱?」,它只有两个选择:

  1. 凭训练记忆「猜」一个价格——可能对,也可能是编的(幻觉);
  2. 老实告诉你「我不知道」。

无论哪种,都不是合格客服该有的样子。真实客服面对「多少钱、有没有货」,是要去系统里查的。这一步「查」,就是工具的用武之地。


二、没有工具,Agent 只能靠猜

先看一组我实测过的真实数据。在还没引入工具之前,我把产品信息硬塞进 prompt,结果 Agent 会「编」价格——同一款产品,它一会儿说 268、一会儿说 328、一会儿说 458,全是幻觉。

这就是大模型的本质:它是在「预测下一个词」,不是「检索事实」。它没接你的数据库,就只能凭概率胡诌。更麻烦的是,它编得还很「自信」——语气笃定、数字精确,一眼看过去跟真的一样。这种「一本正经地胡说八道」,在大模型领域有个专门的名字叫幻觉(Hallucination)。对客服来说,报错一个价格,轻则流失客户,重则引发纠纷。

这里多解释一句幻觉为什么「治不好」。幻觉不是 bug,是大模型的工作方式本身:它学的是「下一个词最可能是什么」,不是「事实是什么」。你没给它数据源,它就用自己的「世界知识」顶上——而模型对具体价格、库存这类实时信息的「世界知识」基本为零。所以根治幻觉只有一条路:别让它猜,给它查的途径。 靠换更大的模型、加提示词「你要诚实」,都只是缓解,不是根治。

工具(Function Calling)就是来治这个病的:给 Agent 一个「查产品」的函数,它需要价格时,就调用这个函数去你的真实数据里查,再把查到的结果组织成回答。从「猜」到「查」,一步之差,天壤之别。


三、工具是什么:五件套

在 AgentScope 2.x 里,一个工具就是 ToolBase 的一个子类,由五部分组成:

部件作用打个比方
name工具名工具箱上贴的标签
description用途说明使用说明书
input_schema入参格式需要你递什么材料
check_permissions权限要不要人工批准
call执行逻辑真正干活的部分

模型靠 name 和 description 判断「该不该调、调哪个」,靠 input_schema 知道「传什么参数」,靠 call 拿到结果。你只要把这五件事写清楚,Agent 就能自己「动手」。完整的工具长什么样,看第六节的 QueryProductInfo。

这五个部件,少了哪个都会出事:description 写糊了,模型就「该调不调」;input_schema 写错了,模型就「传错参数」;check_permissions 设成 ASK,每查一次都要人工点头;call 写错了,查回来的就是错数据。所以工具不是「写个函数就行」,而是「给模型写一份看得懂的说明书」。


四、权限:ALLOW 还是 ASK

一个新手最容易踩的坑,是权限。

check_permissions 能返回三种行为:

行为含义
ALLOW自动放行,Agent 想调就调
ASK每次调用都停下来问人
DENY直接拒绝

如果你用老的 FunctionTool,默认是 ASK——每查一次价格,都要你手动点「同意」,多轮对话会烦死。所以本文用 ToolBase + ALLOW:查询类工具是只读、无害的,直接放行,Agent 才用得顺手。

记住一条经验:只读工具(查价格、查库存)放心 ALLOW;会改数据的工具(下单、删数据)才需要谨慎,甚至 ASK。

这条经验往深了说,就是 Agent 上生产之前必须过的一关:每个工具都要问一句「它有没有副作用」。只读的放行、能改数据的收紧、危险的操作直接 DENY——权限设计不是技术细节,是安全边界。尤其是电商、金融这类场景,一个「下单」工具如果默认 ALLOW,Agent 被恶意输入诱导触发,就是真金白银的损失。工具越少越安全,权限越严越稳妥,这两条是并行不悖的。


五、运行环境(同前三篇,保持一致)

  • 系统:WSL Ubuntu-24.04
  • Python:3.11+,虚拟环境 venv
  • 框架:AgentScope 2.x
  • API Key:DeepSeek / 混元 / 百炼,环境变量注入
wsl -d Ubuntu-24.04
cd /2026_Study/agentscope/
source venv/bin/activate

export DEEPSEEK_API_KEY="sk-xxxx"
export HUNYUAN_API_KEY="sk-xxxx"
export BAILIAN_API_KEY="sk-xxxx"

python 004_tool_call.py

六、完整代码(单文件自包含)

保存为 004_tool_call.py。分两步演示:先直接调工具(验证工具可用),再让 Agent 带工具自主调用。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
AgentScope 2.0 · 给 Agent 装上「手」——工具调用入门
演示:定义一个「查产品」工具,让 Agent 自主决定何时调用它。
  第 1 步:直接调用工具(验证工具本身可用,它就是「查询函数」);
  第 2 步:Agent 带工具,自主判断何时查数据(而不是靠猜/编价格)。
运行环境:WSL Ubuntu-24.04 + Python 3.11+ + AgentScope 2.x
运行命令:python 004_tool_call.py
"""

import os
import asyncio
import gc
import time

from agentscope.credential import (
    DeepSeekCredential, OpenAICredential, DashScopeCredential,
)
from agentscope.model import (
    DeepSeekChatModel, OpenAIChatModel, DashScopeChatModel,
)
from agentscope.agent import Agent
from agentscope.message import Msg, TextBlock
from agentscope.tool import ToolBase, ToolChunk, Toolkit
from agentscope.permission import PermissionDecision, PermissionBehavior


# 敏辰产品数据(真实数据,工具从这里查)
PRODUCT_DB = {
    "玻尿酸保湿霜": {"price": 268, "stock": 50, "skin": "干性/敏感"},
    "净颜清透洁面乳": {"price": 198, "stock": 80, "skin": "油性/混合"},
    "积雪草舒缓精华": {"price": 398, "stock": 30, "skin": "敏感/泛红"},
    "神经酰胺修护乳": {"price": 328, "stock": 45, "skin": "干性/敏感"},
    "美白淡斑精华": {"price": 458, "stock": 20, "skin": "暗沉/色斑"},
}


class QueryProductInfo(ToolBase):
    """查询敏辰产品的价格、库存、适合肤质。

    一个「工具」由五部分组成:
      1. name          —— 工具名,模型靠它识别「该调哪个」
      2. description   —— 工具用途,模型靠它判断「什么时候调」
      3. input_schema  —— 入参格式,告诉模型「要传什么参数」
      4. check_permissions —— 权限,决定「能不能自动执行」
      5. call          —— 真正的执行逻辑
    """

    name = "query_product_info"
    description = "查询敏辰产品的价格、库存、适合肤质。输入产品名称关键词。"
    input_schema = {
        "type": "object",
        "properties": {
            "product_name": {
                "type": "string",
                "description": "产品名称或关键词,如「保湿霜」「洁面乳」",
            },
        },
        "required": ["product_name"],
    }
    is_concurrency_safe = True   # 可并发调用
    is_read_only = True          # 只读,不改数据

    async def check_permissions(self, tool_input, context) -> PermissionDecision:
        # 关键:返回 ALLOW = 自动放行,Agent 无需等待人工确认
        return PermissionDecision(
            behavior=PermissionBehavior.ALLOW,
            message="查询只读,自动放行。",
        )

    async def call(self, product_name: str) -> ToolChunk:
        """查询逻辑:精确匹配 → 模糊匹配 → 未找到提示。"""
        if product_name in PRODUCT_DB:
            info = PRODUCT_DB[product_name]
            result = f"{product_name}:{info['price']}元,库存{info['stock']}件,适合{info['skin']}肤质"
        else:
            matches = [n for n in PRODUCT_DB if product_name in n or n in product_name]
            if matches:
                lines = [f"- {n}:{PRODUCT_DB[n]['price']}元,适合{PRODUCT_DB[n]['skin']}" for n in matches]
                result = "未精确匹配,以下是相关产品:\n" + "\n".join(lines)
            else:
                result = f"未找到与「{product_name}」相关的产品"
        return ToolChunk(content=[TextBlock(text=result)])


def build_models():
    """三个模型,统一 stream=False —— 干净退出第一件套。"""
    deepseek = DeepSeekChatModel(
        credential=DeepSeekCredential(api_key=os.environ["DEEPSEEK_API_KEY"]),
        model="deepseek-v4-flash",
        stream=False,
    )
    hunyuan = OpenAIChatModel(
        credential=OpenAICredential(
            api_key=os.environ["HUNYUAN_API_KEY"],
            base_url="https://tokenhub.tencentmaas.com/v1",
        ),
        model="hy3",
        stream=False,
    )
    bailian = DashScopeChatModel(
        credential=DashScopeCredential(api_key=os.environ["BAILIAN_API_KEY"]),
        model="qwen-max",
        stream=False,
    )
    return {"DeepSeek": deepseek, "混元": hunyuan, "百炼": bailian}


def extract_text(response):
    for block in response.content:
        if getattr(block, "type", None) == "text":
            return block.text
    return ""


async def close_models(models):
    """干净退出第二件套:逐个 await self.client.close()。"""
    closed = 0
    for m in models.values():
        client = getattr(m, "client", None)
        if client is None:
            continue
        try:
            await client.close()
            closed += 1
        except Exception:
            pass
    if closed:
        print(f"\n🧹 已关闭 {closed} 个 HTTP 连接池")


async def demo_manual_tool():
    """第 1 步:不经过 Agent,直接调用工具——验证工具本身可用。"""
    print(f"\n{'=' * 60}\n  第 1 步:直接调用工具(验证工具本身)\n{'=' * 60}")

    tool = QueryProductInfo()
    for name in ["玻尿酸保湿霜", "保湿", "防晒霜"]:
        chunk = await tool.call(name)
        text = chunk.content[0].text
        print(f"\n  查询「{name}」→\n  {text}")


async def demo_agent_tool(model, model_name):
    """第 2 步:Agent 带工具,自主决定何时调用。"""
    print(f"\n{'=' * 60}\n  第 2 步:Agent 带工具自主调用(模型:{model_name})\n{'=' * 60}")

    toolkit = Toolkit(tools=[QueryProductInfo()])

    agent = Agent(
        name="小帮",
        model=model,
        system_prompt=(
            "你叫「小帮」,是「敏辰」的护肤顾问。\n"
            "你可以使用工具 query_product_info 查询产品的价格、库存、肤质。\n"
            "当用户问到具体产品信息(价格/库存/成分)时,请先调用工具查询,再基于结果回答。\n"
            "绝对不要编造产品信息。"
        ),
        toolkit=toolkit,
    )

    questions = [
        "玻尿酸保湿霜多少钱?有货吗?",
        "适合敏感肌的精华有哪些?",
    ]

    for q in questions:
        print(f"\n  👤 用户:{q}")
        msg = Msg(name="user", content=[{"type": "text", "text": q}], role="user")
        response = await agent.reply(msg)
        answer = extract_text(response)
        print(f"  🤖 小帮:{answer}")

    print("\n  💡 观察:Agent 是否自主调用了工具?回答是否基于真实数据(而非编造)?")


def check_keys():
    need = ["DEEPSEEK_API_KEY", "HUNYUAN_API_KEY", "BAILIAN_API_KEY"]
    missing = [k for k in need if not os.environ.get(k)]
    if missing:
        print("❌ 缺少环境变量:" + ", ".join(missing))
        print("   请先 export 对应的 API Key")
        return False
    return True


async def main():
    if not check_keys():
        return

    models = build_models()

    try:
        await demo_manual_tool()
        await demo_agent_tool(models["DeepSeek"], "DeepSeek (deepseek-v4-flash)")
    finally:
        await close_models(models)
        del models
        gc.collect()


if __name__ == "__main__":
    asyncio.run(main())

七、代码拆解:三个关键点

① 工具的「说明书」比「实现」更重要。 name、description、input_schema 这三样,模型是「读」它们来决定要不要调用、怎么调用的。所以 description 一定要写清楚「什么时候该用我」,input_schema 要写清楚「参数叫什么、什么类型」。写糊了,模型就会「该调不调」或「乱传参数」。

② ALLOW 是「自动挡」的钥匙。 只读查询直接放行,Agent 才会在对话里顺滑地查数据;否则每次调用都卡在人工确认,体验全毁。判断标准就一条:这个工具会不会改数据、会不会有副作用。

③ 工具的返回要「喂回」模型。 call 返回的 ToolChunk,框架会自动塞回给模型,模型再基于它组织最终回答。所以 call 里返回的文本要信息完整、格式清晰,模型才好接着写。

顺带厘清一个容易混的概念:工具调用 ≠ 在代码里写死 if-else。写死 if-else 是「代码决定何时查」,工具调用是「模型决定何时查」——后者更灵活,因为模型能理解自然语言的意图(「有货吗」「多少钱」「适合我吗」都能映射到同一个查产品的工具),而 if-else 只能匹配你预设的那几个关键词。


八、运行结果(真实输出)

下面是本文件在 WSL Ubuntu-24.04 里的真实运行结果。

第 1 步(直接调用工具) —— 三个查询,精确命中、模糊匹配、查无此物,各归其位:

查询「玻尿酸保湿霜」→ 玻尿酸保湿霜:268元,库存50件,适合干性/敏感肤质
查询「保湿」→ 未精确匹配,以下是相关产品:- 玻尿酸保湿霜:268元,适合干性/敏感
查询「防晒霜」→ 未找到与「防晒霜」相关的产品

第 2 步(Agent 自主调用) —— 这是重点。问「玻尿酸保湿霜多少钱?有货吗?」,Agent 没有直接编,而是先调工具查真实数据,再回答:

👤 用户:玻尿酸保湿霜多少钱?有货吗?
🤖 小帮:我帮您查询到「玻尿酸保湿霜」的信息如下:
💰 价格:268元
📦 库存:50件(有货)
🧴 适合肤质:干性肤质 / 敏感肤质
这款玻尿酸保湿霜目前有货哦,您可以放心购买。...

问「适合敏感肌的精华有哪些?」,Agent 再次调工具,查到「积雪草舒缓精华」(398 元,敏感/泛红),还主动判断另一款「美白淡斑精华」偏美白淡斑、不适合敏感肌优先推荐——既查了数据,又做了判断:

👤 用户:适合敏感肌的精华有哪些?
🤖 小帮:我为您查到了适合敏感肌的精华产品……这款积雪草舒缓精华是专门针对
敏感肌、泛红肌肤设计的……另外还有一款美白淡斑精华,不过更偏向美白淡斑功效,
建议优先考虑积雪草舒缓精华。

最后一行 🧹 已关闭 3 个 HTTP 连接池,程序干净退出、零收尾报错。

这段结果最有价值的一点:Agent 报出的 268 元、50 件、398 元,全部和代码里的 PRODUCT_DB 一字不差——这就是「查」和「猜」的本质区别。没工具的 Agent 面对同样的问题只能编,编对了是运气、编错了是事故;有工具的 Agent 每一次都回真实数据库里取数,这才是客服该有的样子。


九、总结与下一步

这一篇,你给 Agent 装上了第一只「手」:

只会说(001~003)→ 会查数据(本篇)→ 基于真实数据回答

核心就一句话:工具把 Agent 从「靠猜」拉到「靠查」。name/description/input_schema 是给模型看的说明书,call 是真正干活的地方,ALLOW 让这一切自动运转。

下一篇《AgentScope 2.0 学习笔记:给 Agent 一库「知识」——RAG 检索入门》,我们把「手」升级成「脑外记忆」:工具查的是结构化数据(价格、库存),RAG 查的是非结构化知识(产品详情、成分说明、常见问答)。敬请期待。


十、写在最后:从「猜」到「查」,是 Agent 能不能落地的分水岭

写到这里,想多说一句。幻觉这件事,看起来是个技术问题,其实是产品能不能交付的问题。客户问价格你报错数,一次两次是偶然,三次五次就是信任崩塌——你前面立的所有专业人设,都扛不住一次「编造」的翻车。所以凡是涉及真实数据的 Agent,工具调用不是「进阶技巧」,是上线的及格线。

但工具也带出新问题:你的数据在哪、怎么接、接口怎么设计、权限怎么分级、Agent 误调用怎么办——这些都不是看教程能直接学会的,要对着具体业务一点点磨。如果你正在做一个需要接数据的 Agent(客服、导购、订单查询、内部知识问答都算),卡在工具设计或数据接入上,欢迎来评论区讨论。下一篇「RAG」见。


本文为「AgentScope 2.0 学习笔记」系列第 004 篇,代码已通过 py_compile 语法校验,运行环境见第五节。

GitHub 仓库

系列 : 《AgentScope 2.0 学习笔记》