第五单元——第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 仅用于演示;真实项目必须从服务端验证过的登录身份取得它,不能直接相信客户端随意提交的编号。

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

相关推荐
用户9562562542919 小时前
跟做:Firecrawl 单页转 Markdown 再丢进沙箱总结
python·api
威哥爱编程20 小时前
模拟面试智能体开发实战 02:回答等半分钟,谁还愿意练?把服务端和流式输出搭起来
python·agent·harmonyos
威哥爱编程20 小时前
模拟面试智能体开发实战 01:面试挂了都不知道挂在哪?用 Python 给自己造一个随叫随到的 AI 面试官
python·agent·harmonyos
2601_9679801120 小时前
Python行情API量化 02:顺序拉 20 要 2 秒?并发批量把时间压到 0.5 秒
python·api·限流·并发·量化
伞伞悦读20 小时前
【第81期】特征缩放怎么选:标准化、MinMax 与 Robust 的原理、泄漏对照和完整实验
人工智能·python·深度学习·机器学习
努力的小郑21 小时前
数据查到了,回答去哪儿了?一次百炼 + LangChain 排查实录
后端·langchain·agent
新*21 小时前
从一段 WebSocket 代码聊开:bytes 不是 zlib、binaryType 决定一切,15MB 大消息的零拷贝优化
javascript·python·websocket·性能优化·blob·zlib·arraybuffer
卷无止境1 天前
mathmodel-figure-templates 以及两个上手案例
后端·python
锋行天下1 天前
服务器 Python 2.7 装不了 langgraph-cli?用本地生成 Dockerfile 曲线救国
人工智能·后端·python
一条大祥脚1 天前
深入理解 PyTorch FlexAttention:从 Score Mod 到 Mask Mod 的稀疏加速之道
人工智能·pytorch·python·注意力机制·flex attention·可编程注意力