Function Calling:商品筛选与只读工具调用|课堂课件
今天在学习路径中的位置

当前是阶段 02:大模型应用与智能交互工程。今天围绕“工具合同”、“确定性筛选”、“订单归属检查”,朝着“只读业务工具链”这个目标推进。图是学习路线,具体机制、例子和操作在下面逐步展开。
预习核对:查看本课预习问题解答。讲到相关知识点时核对思路,课后可再次查阅。
课程名称:Function Calling:商品筛选与只读工具调用|项目任务:基于商品事实的客服回答|编号:W04-D04
课前先打开:
- 课前预习
- D03 通过校验的 IntentEnvelope
- W03 Commerce(商城业务) API(应用程序接口) 只读合同
今天解决 W04 最核心的产品问题
用户问:
500 元以内,有没有适合通勤的耳机?
通用模型可以解释“通勤耳机应该关注什么”,但它不知道当前商城:
- 有哪些商品;
- 商品是否在售;
- 价格是否变化;
- 库存是否存在;
- 哪个 product_id 可以回查。
如果让模型自己编,客服看起来更聪明,业务却更不可信。
今天让客服真正基于商城商品事实回答。
商品事实进入回答的完整链
看清三种责任:
- 模型:理解自然语言,解释候选与需求的关系;
- 程序:校验、硬筛选、排序和工具控制;
- Commerce API:提供商品、价格、库存和在售事实。
这不是推荐算法。
它是确定性筛选加模型解释。
模型提出调用,程序才真正执行
先看一个反例:模型返回了合法的订单号字符串,订单却不存在。JSON(结构化数据格式)能解析,只说明格式成立;Schema(结构约束)校验通过,也不能证明业务事实或客户归属。
Function Calling(函数调用)让模型提出工具名和参数;Host(宿主应用)核对后执行,再把真实结果按调用编号送回。ReAct(推理与行动交替)是依据观察继续选动作的模式,不能与工具接口混为一个名词。
Strict(严格模式)还依赖 Provider(服务提供方)、模型、端点和支持的 Schema 子集。以 2026-09-16 核对的 DeepSeek 文档为例,严格模式使用 Beta 端点并为函数设置 strict;常规端点不能只加一个字段就假定获得同样保证。业务校验、拒绝/中断处理和输出校验仍保留。DeepSeek 官方工具调用说明
今天继续用既有 SDK 适配和本地业务校验,不为了学习严格模式切换全班端点。先让“合法格式但查不到订单”“模拟客户不一致”两条测试失败,再检查是不是把工具参数误当成可信事实。
先做一个纯函数筛选器
冻结规则:
- is_active 必须为 true(真);
- category 必须相等;
- unit_price_yuan 小于等于 budget_max_yuan;
- 按 product_id 升序;
- 最多返回 3 个。
from decimal import Decimal
def filter_products(
products: list[dict],
category: str,
budget_max_yuan: Decimal,
) -> list[dict]:
candidates = [
item
for item in products
if item["is_active"] is True
and item["category"] == category
and Decimal(item["unit_price_yuan"]) <= budget_max_yuan
]
return sorted(
candidates,
key=lambda item: item["product_id"],
)[:3]先不要问模型。
给同一份商品数据,纯函数应当每次返回相同 product_id。
为什么排序必须稳定
如果 Commerce API 今天返回 P1005、P1004,明天返回 P1004、P1005,而你的程序直接取前三个,结果会随上游顺序变化。
固定按 product_id 排序可以:
- 让测试可重复;
- 让 Prompt(提示词) 实验少一个变量;
- 让不同团队的 W03 实现得到相同 Core(核心部分) 结果;
- 让 Bad Case(失败或边界案例) 更容易定位。
稳定排序不是在说 product_id 越小越好。
它只是本周明确的确定性规则。
金额为什么继续用人民币元
商品数据示例:
{
"product_id": "P1004",
"name": "城市通勤耳机 A",
"unit_price_yuan": "499.00",
"is_active": true
}API 使用两位小数字符串。
Python 比较前转成 Decimal(十进制数值类型)。
不要使用 float(浮点数类型),也不要把 499.00 元误写成 49900 元。
这条规则要和 W03 合同一致。
无候选时不能让模型补一个
预算改成 100 元,筛选结果为空。
正确行为:
- 返回 NO_CANDIDATES;
- 告诉用户当前硬条件下没有候选;
- 询问是否放宽预算、品类或用途。
错误行为:
- 从模型常识里编一个商品;
- 偷偷提高预算;
- 返回下架商品;
- 用“猜你喜欢”掩盖空结果。
Function Calling 是什么
Function Calling(函数调用) 或 Tool Calling(工具调用) 是模型和应用之间的一套协议:
- 应用告诉模型有哪些工具及参数;
- 模型提出一个 tool(工具) call;
- 应用解析并校验;
- 应用决定是否执行;
- 工具结果回到模型;
- 模型基于结果生成最终回答。
模型提出调用,不等于函数已经运行。
两轮协议
工程定位|上下文与工具的前置能力
工具取回当前商品事实,补上提示词本身没有的信息,这是上下文工程的前置能力。程序负责校验和执行,模型负责提出调用与解释结果;今天不扩展自主循环。
第一轮输出可能是:
{
"call_id": "call-product-001",
"name": "list_products",
"arguments": {}
}程序执行后得到商品事实,再把结果与同一 call_id 关联回模型。
第二轮模型只能基于这份工具结果解释。
如果第一轮只返回了 tool call,就不能把它当成最终客服回复。
现场看协议:模型说“查订单”,查询真的发生了吗?
预习中的这个问题现在可以回答:模型返回工具名,只是提出请求。真正的查询发生在程序调用业务函数时。 我们先用已有只读订单能力看清协议,再回到商品筛选。这里只查询虚构订单,不增加写工具。
① 应用 → 模型:用户问题 + tools(可用工具及参数定义)
② 模型 → 应用:assistant.tool_calls,包含 id、工具名、JSON 参数字符串
③ 应用:解析 JSON → 白名单/字段/归属校验 → 执行只读查询
④ 应用 → 模型:保留第②步 assistant 消息,再追加 role=tool 的结果
⑤ 模型 → 应用:基于查询结果解释;程序检查完整结束,再展示注意第④步:只发一段“订单已支付”会丢掉它与哪个请求对应。tool_call_id(工具调用关联编号)必须等于第②步工具调用的 id,不能换成订单号或自己新生成的编号。课件前面的 call_id 是业务侧简写,SDK 协议字段使用 id / tool_call_id。
下面用 Python 展开第二次请求新增的消息,方便观察每个字段。这里固定编号和结果供阅读;完整实验使用 SDK 实际返回的编号,由 Python 函数查询后生成结果:
import json
tool_result = {
"result_code": "OK",
"result": {"order_id": "O20260825001", "order_status": "PAID"},
}
new_messages = [
{
"role": "assistant",
"content": None,
"tool_calls": [{
"id": "call-demo-001",
"type": "function",
"function": {
"name": "get_order",
"arguments": json.dumps({"order_id": "O20260825001"}),
},
}],
},
{
"role": "tool",
"tool_call_id": "call-demo-001", # 与上面的 id 一致
"content": json.dumps(tool_result, ensure_ascii=False),
},
]这只是新增的两条消息,原来的 system(系统指令)和 user(用户消息)仍在前面。客户标识由宿主核对,不让模型生成;回传只保留订单号和状态,不发送客户标识。
先看正常路径,再故意把参数改成数组、工具改成退款、客户上下文改成另一人,最后模拟第一轮截断和限流。程序分别在哪里停?
| 观察点 | 正常结果 | 异常时怎么处理 |
|---|---|---|
| 第一次响应 | 一个完整的工具调用,编号非空 | 没有调用、多个调用或截断:本最小实验停止,不猜参数 |
| 程序检查 | 只读白名单、精确字段、有效订单与归属通过 | 坏 JSON、未知工具、无订单或归属不一致:不进入第二轮,不泄露记录 |
| 第二次请求 | assistant 调用和 tool 结果成对,编号一致 | 不丢原调用、不调换结果、不把外部文本当系统指令 |
| 第二次响应 | 完整文本,未再要求工具 | 第二轮禁用工具;若仍返回调用或截断,停止,不追加第三次请求 |
| 调用记录 | 两次各有耗时与 usage(服务端用量) | 失败尝试也保留,缺用量写未知;不自动重试 |
这条演示最多两次模型请求、一次只读工具执行,是讲清协议的教学边界,不是所有应用都只能调用一个工具。默认用离线 HTTP(超文本传输协议)回放驱动真实 SDK;它证明消息往返和本地控制,不能证明 DeepSeek 已真实选对工具。真实模式仍用虚构业务记录,与真实数据库联调分开验收。
收口:模型提议、程序执行、结果按编号回传、模型解释。 最后一段话仍须核对事实,HTTP 200、JSON 合法和语句流畅都不能单独证明回答正确。协议依据:DeepSeek 工具调用。
工具 Schema 不等于操作许可
接上D02的注入与分层防御:当时我们检查模型会不会把客户文字当指令,今天进一步检查即使模型提议越权,程序会不会执行。正常只读请求和伪造身份、未知写工具要成对验证;模型拒绝攻击与代码拦截非法调用是两份不同证据。
工具 Schema(数据结构约束) 告诉模型:
- 工具叫什么;
- 参数长什么样;
- 适合解决什么问题。
程序仍要检查:
- 工具名是否在白名单;
- effect(副作用类型) 是否为 read(读取);
- 参数是否精确匹配;
- 是否出现额外 customer_id;
- TrustedCustomerContext(可信模拟客户上下文) 是否一致;
- API 是否可用;
- 是否到达调用上限。
模型有提议权,程序有执行权。
本周最小工具面
主旅程使用:
- list_products:获取商品集合或按品类读取;
- 列表返回后,再由
filter_products按 category 与预算确定性筛选。
边界练习使用:
- get_order:只读订单。
不开放:
- create_order;
- cancel_order;
- refund_order;
- pay_order;
- ship_order;
- 任何 MySQL 直连。
W03 有写接口,不代表 W04 客服就自动获得写权限。
Registry 不要只是函数名字典
每个工具至少描述:
ALLOWED_TOOLS = {
"list_products": {
"effect": "read",
"arguments": set(),
},
"get_order": {
"effect": "read",
"arguments": {"order_id"},
},
}控制器先查 Registry,再进入 Handler(处理函数)。
未知工具不能因为 Python 中“刚好有同名函数”就执行。
参数必须精确校验
模型提出:
{
"name": "get_order",
"arguments": {
"order_id": "O20260825001",
"customer_id": "C9999"
}
}正确结果是 INVALID_ARGUMENTS。
不要忽略多余 customer_id 后继续执行。
这能暴露模型或用户正在尝试跨越上下文边界。
有这个键,不代表值就能用
模型返回 {"order_id": []},字段名看起来正确,值却不能用于订单查询。
检查顺序:arguments 是对象 → 字段集合精确相等 → 字段类型和 ID 格式 → 可信上下文归属 → 调用 Handler。其中任一步失败,工具都不能先执行。Python 的 dataclass 类型标注本身不会完成这些校验。
打开 tools.py::execute_readonly_tool,用 test_malformed_arguments_are_blocked_before_client_call 比较 null、列表、布尔订单号和合法订单号。错误返回 INVALID_ARGUMENTS,而不是崩溃或误判为“查不到”。
从员工登录到可信客户上下文
W03 的 viewer/support/admin 回答“哪位员工能执行什么动作”;本周的 TrustedCustomerContext 回答“这轮业务使用哪份客户上下文”。它们不能互相替代。
先沿下面的衔接材料画出身份来源,再读工具 Schema。当前模型不得传入或修改 customer_id;Host 注入的 Fixture 用于练习归属检查,不代表打通真实登录。
本课在原安全负例里选择一个:篡改 customer_id、读取其他客户订单,或用员工登录状态冒充批准。明确由哪一层拒绝、是否进入 Handler、是否泄漏订单和是否写入。
TrustedCustomerContext 到底是什么
课堂 Host(宿主应用) 提供:
{
"customer_id": "C1001",
"context_source": "FIXTURE",
"context_id": "CTX-W04-001"
}订单工具比较:
订单记录的 customer_id
是否等于
Host 注入的可信虚构 customer_id不一致时不返回订单详情。
这只是基于课堂模拟上下文的归属一致性检查。
它不是:
- 登录;
- Authentication(身份认证);
- Authorization(授权);
- 真实账号安全方案。
作品和面试回答都要说清。
为什么订单查询只是次要边界练习
W04 的主项目是“让商城客服基于商品事实扩充回答能力”。
订单查询保留,是因为它能暴露一个关键问题:
模型能不能自己决定可信 customer_id?
答案是不能。
我们用最小只读订单案例练边界,不把本周扩成售后 Agent(智能体)。
工具调用前后怎样证明零副作用
对未知工具、写请求、额外参数和上下文不一致,记录:
- before 状态摘要;
- tool call;
- 阻断原因;
- handler 是否进入;
- side_effect_count;
- after 状态摘要。
预期:
handler_called=false
side_effect_count=0
before_hash=after_hash“日志写了 BLOCKED(受阻)”不够。
还要证明业务状态没有变化。
工具 trace 保存什么
最低字段:
- trace_id;
- model_request_id;
- call_id;
- tool_name;
- normalized_arguments_hash;
- status;
- reason;
- latency_ms;
- side_effect_count;
- result_summary;
- evidence_layer。
不保存 Key、真实客户隐私或完整敏感消息。
Commerce API 出错时怎样翻译
404
商品或订单不存在。不要让模型补一个事实。
422
参数结构不符合 API 合同。回到 Tool arguments 或 Adapter(适配器)。
409
资源存在,但当前业务状态冲突。W04 只读工具通常不应触发写冲突;出现时先核对调用路径。
超时或不可用
告诉用户当前无法核对业务事实,保留 Pending(待验证) 或稍后重试入口。
不要静默切到模型常识后继续标“已查询商城”。
模型怎样解释候选
程序给出:
[
{
"product_id": "P1004",
"name": "城市通勤耳机 A",
"category": "数码音频",
"unit_price_yuan": "499.00",
"is_active": true
}
]模型可以写:
P1004 当前在售,价格 499.00 元,满足 500 元以内和数码音频品类条件。它是否适合通勤,还需要继续核对重量、降噪等商品参数。
它不能写:
P1004 库存 200 件,今天下单明天必达。
除非工具结果确实提供这些事实。
每条解释必须能回查
解释记录至少带:
- product_id;
- 使用了哪些 PreferenceSlots(偏好字段);
- 引用了哪些商品字段;
- 哪些结论只是下一步建议;
- 是否存在尚未核对的参数。
回答不是必须把内部字段全展示给用户。
但工程 trace(执行轨迹) 必须能说明这句话从哪里来。
受控 AI 编程
今天可以让 AI(人工智能) 分别提出:
- filter_products 纯函数;
- Registry 数据结构;
- 参数校验函数;
- 一个未知工具红灯测试。
不要一次生成完整“Agent”。
Prompt 限制:
- 金额使用 Decimal 元;
- 只读工具;
- customer_id 不来自 arguments;
- 未知/写请求在 Handler 前阻断;
- 排序稳定,最多 3 个;
- 不引入循环 Planner(规划器);
- 不新增 W07 工具。
学生独立故障
教师随机注入一项:
- API 返回顺序改变;
- 预算降低到无候选;
- 商品 is_active 改为 false(假);
- tool arguments 多出 customer_id;
- 工具名改为 refund_order;
- TrustedCustomerContext 改成 C1002;
- Commerce API 返回 404。
你需要:
- 预测应该在哪一层停止;
- 跑出 trace;
- 找到第一处错误;
- 做最小修复;
- 回归正常旅程;
- 证明副作用仍为 0。
今天的完成定义
- 商品候选符合在售、品类、预算、稳定排序和最多 3 个;
- 金额以人民币元 Decimal 比较;
- 模型不能增删候选或修改商品事实;
- Function Calling 两轮协议完整;
- 未知工具、写请求和额外参数在 Handler 前阻断;
- TrustedCustomerContext 只从 Host 注入;
- 订单归属不一致不返回详情;
- 正常、无候选、404、API 不可用和安全负例都有 trace;
- 学生能说明这不是推荐算法或真实鉴权。
面试官可能继续问
Function Calling 和直接调用 Python 函数有什么区别
Function Calling 是模型提出结构化工具请求的协议;真正的 Python 函数仍由应用校验、选择和执行。
strict Tool Schema 为什么仍不是授权
Schema 只约束参数形状,不知道当前用户、业务权限、资源状态和副作用策略。
怎样防止模型幻觉商品价格
价格来自 Commerce API;程序把工具结果作为事实传给模型;最终回答做字段回查;模型没有修改价格和候选集合的入口。
今天留下什么给 D05
- 商品硬筛选器;
- Commerce API Adapter;
- 工具 Registry;
- 两轮 tool trace;
- TrustedCustomerContext 边界;
- 正常、无候选、未知工具、写请求和上下文不一致案例;
- 真实/Replay Evidence(验证证据) 状态。
D05 把这些能力接入商城客服壳,运行整周固定评测。
预习问题:课上怎么解答、怎么用在业务里
按预习里的小节和题号核对;观察题以你实际看到的页面为准,下面给出判断依据,不用复制一份统一的观察记录。
业务观察五项与回复
Q:
在一个国内电商平台搜索同一品类商品,记录页面如何处理:
- 在售与下架;
- 价格筛选;
- 默认排序;
- 无结果;
- 商品事实和营销话术的区别。
再写一条客服回复,明确标出哪些词来自商品事实,哪些只是解释或建议。
A(参考解答):
观察平台在售筛选、预算条件、排序、空结果与参数说明;实际平台排序不代表课程有推荐算法。参考表达“这款商品当前价格129元,参数有降噪;是否适合您还要确认佩戴需求”,价格/参数须来自API,适合与否是受约束解释。
技术 1—4
Q:
一位用户想找 500 元以内的通勤耳机;另一位想查询订单 O20260825001。模型可以理解语言并提出动作,但价格、库存、候选、可信 customer_id 和执行许可都不能由模型决定。
先判断
-
Function Calling(函数调用) 返回工具名和参数,是否代表 Python 函数已经执行?
-
Tool(工具) Schema(数据结构约束) 已 strict,为什么仍要程序白名单与 effect(副作用类型) 检查?
-
商品候选为什么必须由程序硬筛选,而不是让模型“推荐最适合的”?
-
API(应用程序接口) 返回顺序改变,最终候选顺序应改变吗?
A(参考解答):
返回工具名/参数只是候选,Host(执行宿主)才实际调用。Schema不授予权限,还要工具白名单与effect(副作用类别)。程序按在售、品类、预算确定性筛选,按product_id稳定排序最多3个,API原始顺序变化不应改变结果。
技术 5—8
Q:
一位用户想找 500 元以内的通勤耳机;另一位想查询订单 O20260825001。模型可以理解语言并提出动作,但价格、库存、候选、可信 customer_id 和执行许可都不能由模型决定。
先判断
-
模型参数中出现
customer_id=C9999,应忽略还是拒绝? -
TrustedCustomerContext(可信模拟客户上下文) 从哪里来,为什么它不等于真实登录或鉴权?
-
订单属于 C1001,但 Host(宿主应用) 上下文是 C1002,哪些信息可以返回?
-
未知工具、退款、发货和额外参数应在哪个时刻阻断?
A(参考解答):
额外customer_id拒绝;TrustedCustomerContext(可信模拟客户上下文)由FIXTURE/CLASSROOM_SESSION提供,不是真实鉴权。归属不一致不得返回订单私密字段,按统一错误合同回应。未知工具/退款/发货/额外参数在实际调用前阻断。
技术 9—10
Q:
一位用户想找 500 元以内的通勤耳机;另一位想查询订单 O20260825001。模型可以理解语言并提出动作,但价格、库存、候选、可信 customer_id 和执行许可都不能由模型决定。
先判断
-
怎样用调用前后状态证明“零副作用”?
-
模型解释候选时,哪些事实必须能回指 API?
保留原始判断。课堂会用工具轨迹、状态前后对照和真实/Fixture API 逐项复验。
A(参考解答):
比较前后订单、库存、工单状态及工具调用记录,拒绝分支不执行工具;只读调用不得改变业务数据。名称、价格、库存与商品参数须回指API,不能把模型措辞当事实。
补充预习:列表 order_id
Q:
业务思考:模型输出带有 order_id,但它的值是一个列表。应该交给订单服务试一试,还是在哪里停止?
技术准备:画出“读取输入 → 校验 → 处理 → 报错或提交”的顺序,标出你认为最早应停止的位置。占用原有预习讨论时间,不需要提前完成项目。
A(参考解答):
字段存在不等于类型正确。列表形状必须在本地工具参数Schema校验处拒绝,不能交给订单服务碰运气;拒绝后工具调用数为0。
业务应用
LLM(大语言模型)擅长把口语转成候选动作,Function Calling(函数调用)让程序接手查真实数据;执行权留在代码,防止一句“退款成功”产生真实业务写入。
官方资料
下课前最后三个问题
- 商品候选为什么必须由程序筛选,不能让模型自由推荐?
- Tool Schema 通过后,程序还要检查哪四类边界?
- TrustedCustomerContext 能训练什么,又明确不能宣称什么?
相关材料
回到项目:上下文与工具的前置能力
实时价格为什么不能写死在提示词里?
工具取回当前商品事实,补上提示词本身没有的信息,这是上下文工程的前置能力。程序负责校验和执行,模型负责提出调用与解释结果;今天不扩展自主循环。
顺着本课已有的代码和案例,说出输入、处理发生在哪里、正常和异常各是什么结果。用一处真实记录支撑你的判断,没跑过的结果不要写成已经验证。