上一课实现了连续追问。这节增加一条演示规则:
- 用户 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 仅用于演示;真实项目必须从服务端验证过的登录身份取得它,不能直接相信客户端随意提交的编号。
本课记住:用户想买什么,可以由模型提取;用户是谁、享有什么资格,要由程序确认。