系列: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)。但有一个致命短板一直没解决——它只会「说」,不会「做」。
你问它「玻尿酸保湿霜多少钱?」,它只有两个选择:
- 凭训练记忆「猜」一个价格——可能对,也可能是编的(幻觉);
- 老实告诉你「我不知道」。
无论哪种,都不是合格客服该有的样子。真实客服面对「多少钱、有没有货」,是要去系统里查的。这一步「查」,就是工具的用武之地。
二、没有工具,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 学习笔记》
- ChatGPT 玩得很溜,却做不出一个 Agent?
- 多轮对话,让 Agent 记住你(in-token 实证记忆机制)
- 给 Agent 一张「人设卡」(五件套立规矩,AI 导购实测对比)
- 给 Agent 装上「手」——工具调用入门(Function Calling 从猜到查)
- 给 Agent 一库「知识」——RAG 检索入门(补水也能找到保湿)
- AI 答得飞快,一问细节就瞎编?Top-K 调优 + 幻觉测试(附代码)
- 双剑合璧——工具 + RAG,做一个会查会答的客服(从会说到会查会答)
- 一分为多——意图路由与多 Agent 协作(把客服拆成一队)
- 集思广益——多 Agent 投票与结果汇总(用多数人的智慧压住随机性)
- 给 Agent 打个分——评测体系入门(零成本四维质检,编价格一票否决)
- 让 AI 当裁判——LLM Judge 语义评测进阶(规则查不到的「答非所问」,交给 AI 二审)
- 从评测到进化——把评测结果喂回 Prompt(0.75 到 1.0,只差一句 Prompt)
- 从 Demo 到上线——把 Agent 部署成服务(四道闸,把「能跑的脚本」变成「敢上线的服务」)
- 把 Agent 放到云上——容器化部署与上线运营(六步运营闭环,让 Agent 越上线越好用)