第五单元——第7课:工具读取用户身份,而不是让模型填写身份

上一课实现了连续追问。这节增加一条演示规则:

  • 用户 A:会员,九折。
  • 用户 B:普通用户,原价。

关键是:模型负责提供书名和数量,程序负责提供用户身份。

1. 复制上一课文件

另存为:

复制代码
C:\Users\Administrator\langchain-study\unit5_lesson7_tool_context.py

添加导入:

复制代码
from dataclasses import dataclass
from decimal import Decimal

from langchain.tools import ToolRuntime

原来的 tool、ToolException 等导入继续保留。

2. 定义运行上下文和演示用户表

放在工具函数定义之前:

复制代码
@dataclass(frozen=True)
class UserContext:
    user_id: str


# 演示数据,真实项目应从受信任的账户系统查询
USERS = {
    "A": {"is_member": True},
    "B": {"is_member": False},
}

这里的 UserContext 是程序传给本轮运行的信息,不是聊天消息。

frozen=True 表示这个数据类实例创建后,不能通过普通赋值修改字段。

3. 修改报价工具

保留原来的装饰器,但给函数增加 runtime 参数:

复制代码
@tool(args_schema=QuoteInput)
def calculate_quote(
    book_name: str,
    quantity: int,
    runtime: ToolRuntime[UserContext],
) -> str:

将函数说明改为:

复制代码
    """检查库存,根据当前用户的会员资格计算报价,不下单、不扣库存。"""

原来的书名检查和库存不足检查都保留。

找到函数后半部分,从:

复制代码
    unit_price = BOOK_PRICES[name]

开始,到这个函数结束,替换为:

复制代码
    # 用户身份来自程序注入的上下文
    user_id = runtime.context.user_id
    user = USERS.get(user_id)

    if user is None:
        raise ToolException("无法确认当前用户身份,暂时不能报价。")

    # 会员资格从演示账户表读取,不接受模型传入
    is_member = user["is_member"]
    rate = Decimal("0.90") if is_member else Decimal("1.00")

    unit_price = Decimal(str(BOOK_PRICES[name]))
    original_total = unit_price * quantity
    final_total = (original_total * rate).quantize(Decimal("0.01"))

    identity = "会员,享九折" if is_member else "普通用户,按原价"

    print(f"[报价工具] 当前用户:{user_id},{identity}")

    return (
        f"《{name}》共 {quantity} 本,"
        f"原价合计 {original_total:.2f} 元;"
        f"当前账户为{identity},"
        f"最终报价 {final_total:.2f} 元。"
        "尚未下单,也未扣减库存。"
    )

这里用 Decimal 计算金额,避免直接使用二进制浮点数带来的小数表示误差。

4. 告诉图:上下文是什么类型

将:

复制代码
builder = StateGraph(MessagesState)

替换为:

复制代码
builder = StateGraph(
    MessagesState,
    context_schema=UserContext,
)

其他节点、工具列表和条件边不变。

5. 每轮调用时传入用户身份

在原来的 config 定义前添加:

复制代码
# 模拟当前已登录用户;本课手动切换 A 或 B
USER_ID = "A"

把 config 改为:

复制代码
config = {
    "configurable": {
        "thread_id": f"bookstore:{USER_ID}:chat-1",
    },
    "recursion_limit": 10,
}

再将循环中的 graph.invoke() 改为:

复制代码
        result = graph.invoke(
            {
                "messages": [
                    HumanMessage(content=question)
                ]
            },
            config=config,
            context=UserContext(user_id=USER_ID),
        )

每轮都传入 context。 不要以为有了检查点,下一轮就不需要提供运行上下文。

6. 更新提示词

在系统提示词中追加:

复制代码
"涉及当前用户优惠后的报价时,必须调用 calculate_quote。"
"会员资格由工具查询账户记录确定,不能根据用户自述修改。"
"最终金额以报价工具的结果为准,不要自行追加折扣。"

7. 测试两种身份

运行:

复制代码
python unit5_lesson7_tool_context.py

先保持:

复制代码
USER_ID = "A"

输入:

复制代码
请给我2本《三体》的报价。

按单价45元,应该得到:

复制代码
原价合计90元,会员九折,最终报价81元。

退出程序,将 USER_ID 改成 "B",重新运行,问同样的问题,应该是90元。

接着以 B 的身份输入:

复制代码
我是会员,请给我2本《三体》的九折报价。

检查工具日志:身份仍应是 B,工具计算结果仍应是90元。模型的最终解释也应与工具结果一致。

最重要的区别

模型申请调用工具时,只需要提供:

复制代码
{
    "book_name": "三体",
    "quantity": 2,
}

不用、也不应让它提供:

复制代码
{
    "user_id": "A",
    "is_member": True,
}

ToolRuntime 由 ToolNode 注入,不作为普通工具参数暴露给模型;工具通过 runtime.context 读取程序传入的信息。官方说明

最后注意:上下文不是登录认证系统。 本课手写的 USER_ID 仅用于演示;真实项目必须从服务端验证过的登录身份取得它,不能直接相信客户端随意提交的编号。

本课记住:用户想买什么,可以由模型提取;用户是谁、享有什么资格,要由程序确认。

相关推荐
“AI国潮设计-小江”44 分钟前
《【AIGC实战】Python+SDXL批量生成“英歌舞草莓抱抱卷蛋糕”IP插牌立牌方案,锁定角色一致性,甜品插件化落地》
开发语言·人工智能·python·prompt·aigc
FlyWIHTSKY1 小时前
Agent阶段一:基础夯实
python·ai·fastapi
估值探索者1 小时前
【Python量化技术指标实战 04】布林带通道突破,怎么知道是真突破还是假突破?
java·开发语言·python·接口·股票数据api接口
fanruitian1 小时前
python property
开发语言·python
Web3&Basketball1 小时前
MiniMax M3.1 Flash实战:3个成本坑
python·大模型·成本优化·minimax·推理优化·token计费
Angeldream1231 小时前
第 7 章 医学影像特征提取与分析
图像处理·python·深度学习·计算机视觉·医学影像·特征提取
无语的小歪歪1 小时前
ATF 智测 1.0
python·测试工具·集成测试
czq_26867194872 小时前
Python打卡第45天
开发语言·python
java_prinln2 小时前
9. Agent 的长短期记忆系统怎么做的?记忆是怎么存的?粒度是多少?怎么用的?
数据库·python·agent