欢迎光临
我们一直在努力

让智能体真正能查数据(Function Calling 实战)

AgentScope 2.0 工具调用:让智能体真正能查数据(Function Calling 实战)

环境:WSL Ubuntu-24.04 / Python 3.11+ / AgentScope 2.x 代码:单文件自包含,含「模糊描述 vs 精确描述」对照实验 适合:接了工具但模型时调时不调、或者调了填错参数的开发者

给 Agent 接工具,很多人卡在同一个地方:工具写好了,模型有时候调,有时候不调;调了还可能填错参数。

换模型没用,因为问题不在模型。

这篇文章的核心判断:工具定义不是给开发者看的,是给模型看的。 名字、描述、参数三样,都要按"模型需要什么信息才能做决定"来写。

文章目录

  • AgentScope 2.0 工具调用:让智能体真正能查数据(Function Calling 实战)
    • @[toc]
    • 一、没有工具时,它只能猜
    • 二、工具定义的三段信息
      • 2.1 名字:动词 + 对象
      • 2.2 描述:写场景,不写功能
      • 2.3 参数:写约束,不写类型
    • 三、完整代码
    • 四、观察点
    • 五、工具数量与边界
    • 六、权限:ALLOW 还是 ASK
    • 七、常见问题
      • 7.1 工具写了但模型不调用
      • 7.2 调用了但参数填错
      • 7.3 工具返回后模型仍然编造
      • 7.4 工具调用让回答变慢
      • 7.5 返回值该怎么设计
      • 7.6 多个工具会不会打架
    • 八、接口速查
    • 九、下一篇

一、没有工具时,它只能猜

先看基线。不给工具,用户问「玻尿酸保湿霜多少钱」。

模型会给一个数,而且往往带着"以页面为准""活动价可能不同"这类免责话。

看起来像查过,实际是编的。

这不是模型坏了。它的训练目标是"给出有用回答",不是"承认无知"。没有查询手段时,编一个合理数字是它的最优策略。

要改变这个行为,唯一的办法是给它一个能查的地方。


二、工具定义的三段信息

AgentScope 里定义工具要继承 ToolBase,核心是五个属性加两个方法:

成员作用读者
name 模型靠它识别"该调哪个" 模型
description 模型靠它判断"什么时候调" 模型
input_schema 告诉模型"传什么参数" 模型
is_read_only 框架和权限判断用 框架
check_permissions 决定能不能自动执行 框架
call 真正的执行逻辑 系统

前三个是"写给模型看的",也是决定调用成败的部分。

后三个是"写给框架看的":is_read_only 影响权限策略和并发控制,check_permissions 决定要不要人工确认,call 是真正执行的代码。

这个区分很重要:调不对,先看前三个;执行出错,才看后三个。

2.1 名字:动词 + 对象

name = "query_product_info" # 好
name = "product_api" # 差

product_api 只说明"这是个产品相关的东西",模型不知道是查还是改。

加上动词之后,两件事同时变清楚:模型知道这是只读查询,你也能据此决定权限策略。

2.2 描述:写场景,不写功能

这是最关键的一条。

模糊版:

description = "查询敏辰产品的价格、库存、适合肤质。输入产品名称关键词。"

精确版:

description = (
"当用户询问某个产品的价格、库存或适合肤质时使用此工具。"
"参数 product_name 必须是产品全名,例如「玻尿酸保湿霜」。"
"如果用户只说了品类(如「面霜」),先反问具体型号,不要猜测。"
)

精确版多了两件事:什么情况下用、信息不足时怎么办。

第二点尤其重要——它把"用户只说品类"这个高频场景的处理方式写进了工具定义里。

2.3 参数:写约束,不写类型

# 只有类型
"product_name": {"type": "string", "description": "产品名称"}

# 带约束
"product_name": {
"type": "string",
"description": "产品全名,必须是「玻尿酸保湿霜」「净颜清透洁面乳」「积雪草舒缓精华」「神经酰胺修护乳」「美白淡斑精华」之一",
"enum": [...],
}

类型告诉模型"填什么格式",约束告诉模型"填什么内容"。

枚举是最有效的约束形式——它把"该从哪几个里选"直接摆出来,模型不用猜。


三、完整代码

保存为 004_tool_call.py。分三步:验证工具本身、模糊描述版、精确描述版。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
AgentScope 2.0 · 工具调用
三步:
1. 直接调用工具,验证查询逻辑
2. Agent 挂「模糊描述」版工具
3. Agent 挂「精确描述」版工具
"""

import asyncio
import gc
import os

from agentscope.agent import Agent
from agentscope.credential import DeepSeekCredential
from agentscope.message import Msg, TextBlock
from agentscope.model import DeepSeekChatModel
from agentscope.permission import PermissionBehavior, PermissionDecision
from agentscope.tool import ToolBase, ToolChunk, Toolkit

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": "暗沉/色斑"},
}

NAMES = list(PRODUCT_DB.keys())

QUESTIONS = [
"玻尿酸保湿霜多少钱?有货吗?",
"面霜多少钱?", # 只说品类,考验「信息不足时怎么办」
]

def _query(product_name: str) –> str:
"""查询逻辑:精确 → 模糊 → 未找到"""
if product_name in PRODUCT_DB:
i = PRODUCT_DB[product_name]
return "%s:%s元,库存%s件,适合%s肤质" % (
product_name, i["price"], i["stock"], i["skin"])

hits = [n for n in NAMES if product_name in n or n in product_name]
if hits:
lines = ["- %s:%s元,适合%s" % (n, PRODUCT_DB[n]["price"], PRODUCT_DB[n]["skin"])
for n in hits]
return "未精确匹配,相关产品:\\n" + "\\n".join(lines)

return "未找到与「%s」相关的产品" % product_name

class VagueProductTool(ToolBase):
"""A 版:描述只写功能,参数只写类型"""

name = "product_api"
description = "查询敏辰产品的价格、库存、适合肤质。"
input_schema = {
"type": "object",
"properties": {
"product_name": {"type": "string", "description": "产品名称"},
},
"required": ["product_name"],
}
is_read_only = True
is_concurrency_safe = True

async def check_permissions(self, tool_input, context) –> PermissionDecision:
# 只读查询,自动放行
return PermissionDecision(
behavior=PermissionBehavior.ALLOW,
message="只读查询,自动放行。",
)

async def call(self, product_name: str) –> ToolChunk:
return ToolChunk(content=[TextBlock(text=_query(product_name))])

class PreciseProductTool(ToolBase):
"""B 版:描述写场景,参数写约束"""

name = "query_product_info"
description = (
"当用户询问某个产品的价格、库存或适合肤质时使用此工具。"
"参数 product_name 必须是产品全名。"
"如果用户只说了品类(如「面霜」),先反问具体型号,不要猜测。"
)
input_schema = {
"type": "object",
"properties": {
"product_name": {
"type": "string",
"description": "产品全名,必须是以下之一:" + "、".join(NAMES),
"enum": NAMES,
},
},
"required": ["product_name"],
}
is_read_only = True
is_concurrency_safe = True

async def check_permissions(self, tool_input, context) –> PermissionDecision:
return PermissionDecision(
behavior=PermissionBehavior.ALLOW,
message="只读查询,自动放行。",
)

async def call(self, product_name: str) –> ToolChunk:
return ToolChunk(content=[TextBlock(text=_query(product_name))])

def read_text(resp) –> str:
for block in resp.content:
if getattr(block, "type", None) == "text":
return block.text
return ""

async def step1_manual() –> None:
"""第 1 步:不经过 Agent,直接调用,验证查询逻辑"""
print("\\n" + "=" * 56)
print(" 第 1 步:直接调用工具")
print("=" * 56)
tool = PreciseProductTool()
for name in ["玻尿酸保湿霜", "精华", "防晒霜"]:
chunk = await tool.call(name)
print("\\n 查「%s」→ %s" % (name, chunk.content[0].text))

async def step2_agent(model, label: str, tool) –> None:
"""第 2 步:Agent 挂工具,自主决定何时调用"""
print("\\n" + "=" * 56)
print(" " + label)
print("=" * 56)

agent = Agent(
name="小帮",
model=model,
system_prompt=(
"你叫「小帮」,是敏辰的护肤导购。"
"回答涉及产品价格、库存、适合肤质时,先调用工具查询,不要编造。"
),
toolkit=Toolkit(tools=[tool]),
)

for q in QUESTIONS:
resp = await agent.reply(
Msg(name="user", content=[{"type": "text", "text": q}], role="user")
)
print("\\n 用户:%s" % q)
print(" 小帮:%s" % read_text(resp))

async def main() –> None:
if not os.environ.get("DEEPSEEK_API_KEY"):
print("[x] 缺少 DEEPSEEK_API_KEY")
return

model = DeepSeekChatModel(
credential=DeepSeekCredential(api_key=os.environ["DEEPSEEK_API_KEY"]),
model="deepseek-v4-flash",
stream=False,
)

try:
await step1_manual()
await step2_agent(model, "A 组:模糊描述(product_api)", VagueProductTool())
await step2_agent(model, "B 组:精确描述(query_product_info)", PreciseProductTool())
finally:
client = getattr(model, "client", None)
if client is not None:
try:
await client.close()
except Exception:
pass
del model
gc.collect()

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


四、观察点

跑完对比两组输出,重点看第二个问题「面霜多少钱」。

A 组(模糊描述) 通常会直接给一个价格,或者含糊地说"我们有多款面霜"。

B 组(精确描述) 大概率会反问"您问的是哪一款",因为它拿到的参数是枚举,只说了品类时它无法填。

这个差异不是模型变聪明了,是工具定义里写清了"信息不足时怎么办"。


五、工具数量与边界

工具不是越多越好。

我试过给一个 Agent 接 12 个工具,结果是频繁调错:问价格去查物流,问库存去查优惠券。

工具越多,选择越难。经验上限是 5 个,超过就拆成多个 Agent。

更基础的一个问题是:什么该做成工具。

需求做成工具原因
查价格 是 会变,编错要赔钱
查库存 是 实时,必须准
查物流 是 用户最关心,必然实时
产品功效 否 稳定,放知识库
品牌故事 否 稳定,放人设
护肤常识 否 通用知识,模型自己会

判断标准:会变的、必须准的走工具;不变的、可以模糊的走知识库。

把稳定知识也做成工具,会有两个代价:每次对话多一次调用(变慢变贵),以及调用越频繁越容易出错。


六、权限:ALLOW 还是 ASK

check_permissions 返回 ALLOW 表示自动执行,ASK 表示需要确认。

我第一版全设成 ALLOW,图省事。后来出了事故:一个"提交售后申请"的工具在用户还没确认时被调用,直接生成了一张工单。

现在的规则:

类型权限例子
只读查询 ALLOW 查价格、查库存、查物流
写入操作 ASK 提交工单、改地址、发优惠券
涉及金额 ASK 退款、改价、发放余额

判断标准很土:这个动作做错了,能不能撤回。

能撤回的放行,不能撤回的问一句。


七、常见问题

7.1 工具写了但模型不调用

三个原因,按概率排:

  • 描述没写场景。 只写"查询产品信息",模型不知道该在什么时候用。
  • 人设里没有要求。 system_prompt 里要写一句"涉及价格先调工具,不要编造",否则模型会优先用自己的知识。
  • 工具名太抽象。 product_api 这种名字,模型很难判断用途。
  • 排查顺序就按这三条走,别一上来就换模型。换模型最贵,而且通常解决不了描述层面的问题。

    7.2 调用了但参数填错

    参数没有约束。只写 type: string,模型会填"保湿霜"而不是"玻尿酸保湿霜"。

    解法是加 enum 或 description 里列出可选值。

    7.3 工具返回后模型仍然编造

    两种情况:

    • 返回值太长,模型没读全。把返回结构压短,只留关键字段。
    • 返回值是自然语言,模型当成"参考资料"而不是"事实"。尽量返回结构化内容。

    7.4 工具调用让回答变慢

    每次调用至少多一次模型往返。查询类工具通常 1~3 秒,写入类更久。

    如果业务对延迟敏感,把高频查询做成缓存(比如价格 5 分钟缓存),而不是砍工具。

    7.5 返回值该怎么设计

    返回值对调用成功率的影响,比大多数人想的大。

    三种常见写法,效果差别明显:

    # 差:返回一大段自然语言
    return "玻尿酸保湿霜,售价268元,目前仓库里还有50件,适合干性和敏感性皮肤使用,质地清爽不油腻……"

    # 好:结构化 + 短
    return "玻尿酸保湿霜 | 价格268元 | 库存50 | 适合干性/敏感"

    # 最好:JSON
    return '{"name":"玻尿酸保湿霜","price":268,"stock":50,"skin":"干性/敏感"}'

    第一种的问题是信息密度低,模型要从中抽取关键字段,容易漏。

    第三种最好,但要注意长度——返回字段太多会撑大上下文。

    我的做法是只返回"模型回答需要的那几个字段",其余字段留在后端日志里。

    7.6 多个工具会不会打架

    会。

    典型场景是两个工具的描述都包含"查询":

    • query_product_info:查产品价格库存
    • query_order_status:查订单状态

    用户问「我买的那款还有货吗」,模型可能去查订单。

    这不是描述写得不好,是"查询"这个动作天然重叠。

    三种解法,按代价排:

  • 描述里加区分词:「查产品(不查订单)」,治标。
  • 工具名加前缀:product_query / order_query,让命名空间可见。
  • 拆 Agent:售前只挂产品工具,售后只挂订单工具。最彻底。
  • 我最后选第三种。因为工具重叠的根因是业务边界不清,靠 prompt 补不回来。


    八、接口速查

    成员类型说明
    name str 工具名,建议动词开头
    description str 写"什么时候用",不写功能说明
    input_schema dict JSON Schema,用 enum 做约束
    is_read_only bool 只读标记,影响权限判断
    is_concurrency_safe bool 是否可并发调用
    check_permissions() async 返回 ALLOW / ASK
    call() async 执行逻辑,返回 ToolChunk
    Toolkit(tools=[…]) — 工具集合
    Agent(…, toolkit=…) — 把工具挂到 Agent 上

    九、下一篇

    工具让 Agent 能查实时数据,但有些问题实时接口答不了——比如"你们的成分表和竞品比有什么优势",这需要读文档。

    下一篇讲 RAG:给 Agent 接一个知识库,以及最容易出错的那一步(切分,不是模型)。


    代码已通过 py_compile 语法校验。工具调用行为受模型影响,不同版本表现可能有差异。

    赞(0)
    未经允许不得转载:171主机测评 » 让智能体真正能查数据(Function Calling 实战)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址