第五单元——第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. 分清三个不同职责

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

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

本课还有两个边界:

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

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

相关推荐
星恒随风5 小时前
MySQL 从零入门(五):MySQL 内置函数详解——日期、字符串、数学函数与实战案例
linux·数据库·笔记·sql·mysql
小雉系统5 小时前
BcoreOS安全通信架构:FsSession 抗算法后门安全设计
算法·安全·架构
金銀銅鐵5 小时前
带您看懂字节码指令(二):空循环里有什么
java·后端·ai编程
Miya_Ye6 小时前
AI工具 Phoenix
ai·工具
吴名氏.7 小时前
Java转AI Agent教程(第十三周)混合检索与重排
java·数据库·人工智能·ai·ai agent
爱码少年7 小时前
Springboot下如何将CSV文件数据导入H2、Mysql数据库表
数据库·mysql·csv
.痞老板.8 小时前
java ee--多线程(进阶)
java·java-ee
可乐ea8 小时前
智能体记忆该拆成几层:笔记、记忆表示与注入上下文
服务器·数据库·笔记·ai智能体·提示词工程·上下文工程·记忆系统
深入云栈9 小时前
Netty 4.2.x 源码深度解析 (二十三):ByteBuf 核心设计 —— 索引、类型与零拷贝
java·后端·架构