第五单元——第9课:防止同一次操作重复执行

上一课加入了人工确认,但还有一个问题:

用户已经确认,程序保存成功,却在返回结果前出错了。重试时,会不会再保存一遍?

这节学习幂等性:对同一个请求执行多次,不重复产生业务记录。

我们只保存本地的模拟订单记录,不创建真实订单,不扣款、不扣库存。

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. 分清三个不同职责

机制 解决的问题
人工确认 这次操作是否获得批准
检查点 图运行到哪里,如何继续
幂等控制 同一个请求重跑时,是否重复写入

有人工确认,不代表不会重复执行;有检查点,也不代表外部写入自动防重。

本课还有两个边界:

  • 用户再次提出下单请求,模型产生了新的工具调用编号,就属于新请求,不会被这套规则自动合并。
  • 这套代码只保护本地模拟记录。若以后接真实订单或支付接口,还需要下游接口配合稳定的业务请求编号,不能认为本地数据库已经保护了所有外部操作。

本课验收重点很简单:同一个用户、同一个请求编号、同样的内容,重复调用后仍返回同一个模拟记录编号。

相关推荐
山东孙燕姿1 小时前
维修工单的转派与协同:两个人一起上
大数据·运维·服务器·网络·人工智能
zhangdaliangmvp1 小时前
快速排序的学习理解:从基础分治到三路划分优化
c++·学习·算法
OxYGC1 小时前
Spring 面经系列 · 第 6 篇:Spring MVC 请求全流程、父子容器与零配置原理
java·spring·mvc
code2cat1 小时前
Java高级篇之JDBC:连接数据库,完成查询、更新与事务
java·开发语言·数据库
乐迪信息1 小时前
AI算法盒子船体烟火检测:港口火灾早预警
人工智能·深度学习·算法
*长铗归来*1 小时前
MySQL新学知识(三)MySQL 事件(EVENT)
数据库·mysql
bksczm1 小时前
Redis数据结构之string
java·redis·mybatis
cc_瀚海知行1 小时前
资料检索 1|直接放入全文、按需读文件和先检索,有什么区别?
java·人工智能·ai
lisw051 小时前
发酵算法两大应用方向解析
算法·数学建模