上一课加入了人工确认,但还有一个问题:
用户已经确认,程序保存成功,却在返回结果前出错了。重试时,会不会再保存一遍?
这节学习幂等性:对同一个请求执行多次,不重复产生业务记录。
我们只保存本地的模拟订单记录,不创建真实订单,不扣款、不扣库存。
1. 先做一个独立实验
新建:
C:\Users\Administrator\langchain-study\unit5_lesson9_idempotency.py
写入:
import sqlite3
from contextlib import closing
from pathlib import Path
DB_PATH = Path(__file__).with_name("simulated_orders.sqlite")
def save_simulated_order(
user_id: str,
request_id: str,
book_name: str,
quantity: int,
total_yuan: str,
) -> tuple[int, bool]:
"""返回模拟记录编号,以及本次是否新建。"""
with closing(sqlite3.connect(DB_PATH)) as conn:
# 正常退出提交事务,发生异常则回滚
with conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS simulated_orders (
id INTEGER PRIMARY KEY,
user_id TEXT NOT NULL,
request_id TEXT NOT NULL,
book_name TEXT NOT NULL,
quantity INTEGER NOT NULL,
total_yuan TEXT NOT NULL,
UNIQUE(user_id, request_id)
)
""")
cursor = conn.execute("""
INSERT INTO simulated_orders (
user_id, request_id, book_name,
quantity, total_yuan
)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(user_id, request_id) DO NOTHING
""", (
user_id,
request_id,
book_name,
quantity,
total_yuan,
))
created = cursor.rowcount == 1
row = conn.execute("""
SELECT id, book_name, quantity, total_yuan
FROM simulated_orders
WHERE user_id = ? AND request_id = ?
""", (user_id, request_id)).fetchone()
# 同一请求编号不能悄悄变成另一笔业务
if row[1:] != (book_name, quantity, total_yuan):
raise ValueError(
"同一请求编号对应的书名、数量或金额发生变化。"
)
order_id = row[0]
return order_id, created
if __name__ == "__main__":
for attempt in range(1, 3):
order_id, created = save_simulated_order(
user_id="A",
request_id="demo-request-1",
book_name="三体",
quantity=2,
total_yuan="81.00",
)
action = "新建记录" if created else "复用已有记录"
print(
f"第{attempt}次调用:{action},"
f"模拟记录编号={order_id}"
)
这段实验只用 Python 标准库,不调用模型。
2. 连续运行,观察结果
python unit5_lesson9_idempotency.py
第一次使用空数据库时,预期类似:
第1次调用:新建记录,模拟记录编号=1
第2次调用:复用已有记录,模拟记录编号=1
再运行一次,应该两次都是"复用已有记录",编号不变。
数据库会保存在:
C:\Users\Administrator\langchain-study\simulated_orders.sqlite
3. 真正防重复的是哪两处?
第一处:
UNIQUE(user_id, request_id)
它要求同一个用户的同一个请求编号,只能出现一次。
第二处:
ON CONFLICT(user_id, request_id) DO NOTHING
如果该请求已经存在,就不再插入。SQLite 官方说明
这不是"先查有没有,再决定插入",而是让数据库的唯一约束负责拦截重复写入。
此外,我们还比较:
row[1:] != (book_name, quantity, total_yuan)
防止同一个请求编号第一次代表"买2本",第二次却变成"买3本"。这种情况应报错,而不是默默复用或覆盖。
4. 接回上一课的人工确认工具
将上一课另存为:
C:\Users\Administrator\langchain-study\unit5_lesson9_order_agent.py
在顶部添加:
from unit5_lesson9_idempotency import save_simulated_order
两个文件放在同一目录。
因为独立实验放在:
if __name__ == "__main__":
下面,所以导入函数时不会自动执行那两次测试。
在 simulate_order 中,保留原来的人工确认和拒绝分支:
if approved is not True:
return "用户拒绝了本次模拟下单,未执行任何下单操作。"
把它后面的模拟成功返回,替换为:
# 同一个工具调用在恢复、重试时复用这个编号
request_id = runtime.tool_call_id
if not request_id:
raise ToolException("缺少工具调用编号,无法安全保存模拟记录。")
try:
order_id, created = save_simulated_order(
user_id=user_id,
request_id=request_id,
book_name=name,
quantity=quantity,
total_yuan=f"{total:.2f}",
)
except ValueError as error:
raise ToolException(str(error)) from error
action = "已新增模拟记录" if created else "已复用原模拟记录"
return (
f"{action},编号为 {order_id}。"
f"《{name}》{quantity} 本,模拟金额 {total:.2f} 元。"
"仅保存了本地练习记录,未创建真实订单,"
"未扣款,未扣库存。"
)
runtime.tool_call_id 是框架提供的当前工具调用编号,不需要让模型把它作为普通参数填写。工具运行上下文说明
5. 分清三个不同职责
| 机制 | 解决的问题 |
|---|---|
| 人工确认 | 这次操作是否获得批准 |
| 检查点 | 图运行到哪里,如何继续 |
| 幂等控制 | 同一个请求重跑时,是否重复写入 |
有人工确认,不代表不会重复执行;有检查点,也不代表外部写入自动防重。
本课还有两个边界:
- 用户再次提出下单请求,模型产生了新的工具调用编号,就属于新请求,不会被这套规则自动合并。
- 这套代码只保护本地模拟记录。若以后接真实订单或支付接口,还需要下游接口配合稳定的业务请求编号,不能认为本地数据库已经保护了所有外部操作。
本课验收重点很简单:同一个用户、同一个请求编号、同样的内容,重复调用后仍返回同一个模拟记录编号。