Function Calling:商品筛选与只读工具调用|课堂课件

今天在学习路径中的位置

W04-D04 当天能力地图:工具合同、确定性筛选、订单归属检查

当前是阶段 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(工具调用) 是模型和应用之间的一套协议:

  1. 应用告诉模型有哪些工具及参数;
  2. 模型提出一个 tool(工具) call;
  3. 应用解析并校验;
  4. 应用决定是否执行;
  5. 工具结果回到模型;
  6. 模型基于结果生成最终回答。

模型提出调用,不等于函数已经运行。


两轮协议

工程定位|上下文与工具的前置能力

工具取回当前商品事实,补上提示词本身没有的信息,这是上下文工程的前置能力。程序负责校验和执行,模型负责提出调用与解释结果;今天不扩展自主循环。

Function Calling 两轮协议

第一轮输出可能是:

{
  "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。

你需要:

  1. 预测应该在哪一层停止;
  2. 跑出 trace;
  3. 找到第一处错误;
  4. 做最小修复;
  5. 回归正常旅程;
  6. 证明副作用仍为 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 和执行许可都不能由模型决定。

先判断

  1. Function Calling(函数调用) 返回工具名和参数,是否代表 Python 函数已经执行?

  2. Tool(工具) Schema(数据结构约束) 已 strict,为什么仍要程序白名单与 effect(副作用类型) 检查?

  3. 商品候选为什么必须由程序硬筛选,而不是让模型“推荐最适合的”?

  4. API(应用程序接口) 返回顺序改变,最终候选顺序应改变吗?

A(参考解答):

返回工具名/参数只是候选,Host(执行宿主)才实际调用。Schema不授予权限,还要工具白名单与effect(副作用类别)。程序按在售、品类、预算确定性筛选,按product_id稳定排序最多3个,API原始顺序变化不应改变结果。

技术 5—8

Q:

一位用户想找 500 元以内的通勤耳机;另一位想查询订单 O20260825001。模型可以理解语言并提出动作,但价格、库存、候选、可信 customer_id 和执行许可都不能由模型决定。

先判断

  1. 模型参数中出现 customer_id=C9999,应忽略还是拒绝?

  2. TrustedCustomerContext(可信模拟客户上下文) 从哪里来,为什么它不等于真实登录或鉴权?

  3. 订单属于 C1001,但 Host(宿主应用) 上下文是 C1002,哪些信息可以返回?

  4. 未知工具、退款、发货和额外参数应在哪个时刻阻断?

A(参考解答):

额外customer_id拒绝;TrustedCustomerContext(可信模拟客户上下文)由FIXTURE/CLASSROOM_SESSION提供,不是真实鉴权。归属不一致不得返回订单私密字段,按统一错误合同回应。未知工具/退款/发货/额外参数在实际调用前阻断。

技术 9—10

Q:

一位用户想找 500 元以内的通勤耳机;另一位想查询订单 O20260825001。模型可以理解语言并提出动作,但价格、库存、候选、可信 customer_id 和执行许可都不能由模型决定。

先判断

  1. 怎样用调用前后状态证明“零副作用”?

  2. 模型解释候选时,哪些事实必须能回指 API?

保留原始判断。课堂会用工具轨迹、状态前后对照和真实/Fixture API 逐项复验。

A(参考解答):

比较前后订单、库存、工单状态及工具调用记录,拒绝分支不执行工具;只读调用不得改变业务数据。名称、价格、库存与商品参数须回指API,不能把模型措辞当事实。

补充预习:列表 order_id

Q:

业务思考:模型输出带有 order_id,但它的值是一个列表。应该交给订单服务试一试,还是在哪里停止?

技术准备:画出“读取输入 → 校验 → 处理 → 报错或提交”的顺序,标出你认为最早应停止的位置。占用原有预习讨论时间,不需要提前完成项目。

A(参考解答):

字段存在不等于类型正确。列表形状必须在本地工具参数Schema校验处拒绝,不能交给订单服务碰运气;拒绝后工具调用数为0。

业务应用

LLM(大语言模型)擅长把口语转成候选动作,Function Calling(函数调用)让程序接手查真实数据;执行权留在代码,防止一句“退款成功”产生真实业务写入。

官方资料

下课前最后三个问题

  1. 商品候选为什么必须由程序筛选,不能让模型自由推荐?
  2. Tool Schema 通过后,程序还要检查哪四类边界?
  3. TrustedCustomerContext 能训练什么,又明确不能宣称什么?

相关材料

课前预习|课后复习

回到项目:上下文与工具的前置能力

实时价格为什么不能写死在提示词里?

工具取回当前商品事实,补上提示词本身没有的信息,这是上下文工程的前置能力。程序负责校验和执行,模型负责提出调用与解释结果;今天不扩展自主循环。

顺着本课已有的代码和案例,说出输入、处理发生在哪里、正常和异常各是什么结果。用一处真实记录支撑你的判断,没跑过的结果不要写成已经验证。