资料整理Agent调用“完成任务”工具,工具返回了ok: false。按理说,这单应该还停在草稿。管理员打开数据库,却发现任务已经变成“就绪”,对应的资料清单却没有生成。
这次不必先怀疑模型撒谎。工具也许确实报告了失败,只是它把异常接住的位置,刚好让数据库事务看见了一次正常退出。
本文只回答一个问题:一组数据库操作必须一起成功时,怎样避免“失败已经变成JSON,前半段却被提交”?这属于Agent工具服务的执行支撑层问题,不是提示词技巧。
AI原创概念插画:日光石拱水房内,两格青釉水池分别盛着琥珀色水和露出花砖池底,各有一只红纸船;前景是有裂缝的蓝釉圆片与黄铜阀轮。画面只作两边状态不一致的艺术隐喻,不证明真实水流、事务执行或故障过程。
一、数据库不知道你的ok: false是什么意思
先把三个信号拆开:
- 业务结果:任务是否满足“状态就绪,而且清单存在”;
- Python控制流:退出代码块时,有没有尚未处理的异常;
- 数据库结果:事务最终提交了,还是回滚了。
返回一个失败字典,只改变第一类信号的表达。事务上下文管理器通常不会打开字典,再替你判断这次是否应该回滚。
Python 3.14的sqlite3文档明确说明:连接上下文退出时,若代码块正常结束,就提交打开的事务;若未处理异常离开代码块,或者提交失败,则回滚。没有打开的事务或autocommit=True时,上下文不做这项处理。[1]
因此,下面这条推理并不成立:
工具返回失败 → 数据库一定没改。
更准确的问法是:失败被转换成返回值之前,负责事务结算的那一层,有没有收到正确的失败信号?
这也不是数据库破坏了原子性。若程序只执行了第一条更新,随后正常提交,那么数据库原子提交的就是这份“只包含第一条更新”的事务。应用本来要求两件事一起完成,却没有把这项业务要求一直保留到提交边界。[2]
二、一个可以跟着运行的小实验
虚构的资料任务有两张表:jobs保存状态,manifests保存清单登记。业务约定:完成操作必须同时把任务设为ready并登记一条清单;如果清单尚未获准,本次修改应整体撤销。
为了暴露故障,示例故意先更新状态,再进行业务条件检查。真实服务应尽量提前完成可提前做的验证,但仍不能因此省略事务内的失败处理。
**以下为本地标准库教学实验,不是Agent、模型或线上数据库实测。要求Python 3.12及以上,以使用显式的autocommit参数。**本次实际运行环境是Python 3.13.5、SQLite 3.53.4。代码使用内存库;后文另说明磁盘库核验。
import sqlite3
class NotReady(Exception):
pass
def seed():
db = sqlite3.connect(":memory:", autocommit=False)
db.execute("CREATE TABLE jobs(id TEXT PRIMARY KEY, state TEXT)")
db.execute("CREATE TABLE manifests(job_id TEXT PRIMARY KEY)")
db.execute("INSERT INTO jobs VALUES('job-1', 'draft')")
db.commit()
return db
def change(db, approved):
db.execute("UPDATE jobs SET state='ready' WHERE id='job-1'")
if not approved:
raise NotReady("manifest not approved")
db.execute("INSERT INTO manifests VALUES('job-1')")
def bad(db, approved):
with db:
try:
change(db, approved)
except NotReady:
return {"ok": False}
return {"ok": True}
def good(db, approved):
try:
with db:
change(db, approved)
except NotReady:
return {"ok": False}
return {"ok": True}
for name, tool, approved in [
("bad-failure", bad, False),
("good-failure", good, False),
("good-success", good, True),
]:
db = seed()
try:
result = tool(db, approved)
state = db.execute("SELECT state FROM jobs").fetchone()[0]
count = db.execute("SELECT count(*) FROM manifests").fetchone()[0]
print(name, result, state, count)
finally:
db.close()本次独立执行这段代码,输出如下:
bad-failure {'ok': False} ready 0
good-failure {'ok': False} draft 0
good-success {'ok': True} ready 1看第一行:工具诚实返回失败,数据库却留下了ready,清单数量仍为零。
bad()里的return不是跳过with的清理过程。返回之前,上下文管理器仍会退出;但异常已经被内部的except消化掉,此时它收到的是正常退出,于是提交之前那条更新。
good()只移动了一道边界:让NotReady先离开事务代码块,触发回滚,再在外侧把这个已知业务失败转成工具结果。成功路径则要等with退出、提交完成以后,才返回成功。
示例只捕获专门定义的NotReady,不是把所有数据库异常、编程错误都塞进同一个“条件未满足”。其他异常仍向外传播,由服务的错误边界分别处理和记录。
三、改了except,为什么还不算验收完成?
只看失败字典,会把坏实现和好实现判成一样。只在同一个连接里查询,也不足以单独证明所见变化已经提交。
本次还用独立的本地临时磁盘数据库复核:每种情况从相同种子状态开始,执行后关闭连接,再用新连接读取。实际结果如下:
| 本地场景 | 工具结果 | 重新打开后的任务状态 | 清单条数 |
|---|---|---|---|
| 块内接住业务异常 | 失败 | ready | 0 |
| 块外接住业务异常 | 失败 | draft | 0 |
| 块内处理、无异常成功 | 成功 | ready | 1 |
| 块外处理、无异常成功 | 成功 | ready | 1 |
| 改为自动提交,使用块外处理,但业务失败 | 失败 | ready | 0 |
最后一行是必要的反例:把连接设成autocommit=True以后,第一条更新已经独立生效。即使后面的异常正确离开with db,这里的连接上下文也不会把它撤销。[1]
所以,正确的代码缩进不能替代正确的事务配置。要核对的是实际连接、实际驱动、实际模式,不能只搜索函数里有没有with这个单词。
扩展实验还验证了两点:意料之外的唯一键冲突会从good()传播出去,并回滚本轮状态修改;事务中追加到普通Python列表的标记则不会被数据库回滚。这些都是受控本地夹具的结果,不是崩溃恢复、网络丢包、生产并发或持久性压力测试。
四、不要把SQLite实验直接套成所有驱动的规则
Psycopg文档同样区分了连接上下文、自动提交和显式的conn.transaction():事务上下文正常退出时提交,异常退出时回滚;若已有外层事务,内部事务上下文可能只管理一个保存点,并不负责最终提交。[3]
这支持一个通用的审查方向:**弄清谁拥有最终提交权,以及错误跨过了哪一层。**但不能因此宣称不同数据库遇错后的状态完全相同。
尤其要区分:
- **应用自己抛出的业务异常。**本例的
NotReady没有执行失败的SQL;数据库未必知道应用已判定整单失败。 - **数据库语句本身报错。**PostgreSQL可能令当前事务进入中止状态,需要回滚或回到保存点才能继续;SQLite的错误对当前语句和整个事务的影响,也要按具体错误与冲突策略判断。[2][3][4]
本篇没有运行Psycopg或PostgreSQL,不声称同一段失败代码在它们上面都会得到相同的局部提交结果。
另外,Python的with db在这里负责事务结算,不负责关闭SQLite连接。示例因此显式调用close()。不能从另一种驱动的连接上下文行为反推本库也一样。[1]
五、工具服务应把哪一道边界留住?
对于“本次必须全成或全不成”的单库操作,可以采用这样的组织方式:
- 在事务外做能够提前完成的输入验证和授权检查;
- 进入明确的事务范围,在同一连接上完成相关写入;
- 检查必要的不变量,例如预期行是否真的被更新,关联清单是否满足要求;不满足就让事务层明确走回滚路径;
- 等事务结算完成,再把已知业务失败转换为稳定的工具错误结构;
- 最后核对业务终态,不用“函数没有抛异常”替代“该有的记录都存在”。
这里没有要求所有程序必须使用异常实现业务失败。显式rollback()、受支持的回滚标记或驱动专用机制也可以,前提是它们在实际契约下有效,而且不会在回滚之后误继续写入、又形成一次正常提交。
保存点则适合另一类任务:允许单个子项失败、其他子项继续。它不是默认应对所有异常的技巧。若用户要求的是整批原子完成,就不能悄悄把它改成“成功几项算几项”。[2][3]
还有两条边界不能混掉:
- 数据库事务只覆盖它参与管理的资源。已经发送的邮件、写出的文件、远端工具调用,不会因为本地SQL回滚就自动撤销。
- 若提交阶段遇到连接故障,不能把“调用报错”直接翻译成“肯定没提交”。结果不明应保留任务标识和对账入口,确认远端状态后再决定是否重试,而不是把本地教学例子的确定回滚结论扩展到所有故障。
六、可直接复用的验收清单
审核一个会写数据库的Agent工具时,可以逐项问:
- 业务要求是整单原子完成,还是允许部分成功?有无明确约定?
- 哪些语句属于同一事务?是否同一个连接?有没有中途提交?
- 当前驱动的自动提交、连接上下文、事务上下文和保存点分别做什么?
- 业务失败是异常、返回值,还是回滚标记?事务层真的能看见吗?
- 已知业务错误的转换是否发生在正确的结算边界之外?未知错误有没有被过度捕获?
- “失败后没变化”是否通过重读验证?成功路径是否同时满足所有业务条件?
- 是否加入了自动提交反例、后半步失败和意外数据库错误的测试?
- 数据库以外的副作用及提交结果未知,是否有独立处理方案?
**失败字典是给调用者看的;回滚信号是给事务管理层看的。两者可以对应,却不会自动等价。**把这条对应关系做实,才不会出现Agent已经收到“失败”,数据库却悄悄保留半份成果。
来源与适用范围
本文由AI创作。资料实际检索截止:2026年10月27日14:45 UTC。下面是机制文档,不是当日新品或性能公告;文档版本不代表本机安装版本。
- Python 3.14:sqlite3连接上下文管理器与autocommit事务控制。按3.14文档阅读相关完整章节;动态维护页,无单独章节发布日期。代码实际运行于3.13.5。
- PostgreSQL 18:Transactions。网页正文片段之外,从官方REL_18_STABLE源文档补读事务完整一节;分支会更新,未锁提交版本。用于原子性、回滚与保存点解释,不作为本人数据库实测。
- Psycopg 3:Transactions management。实际阅读连接管理、自动提交、事务上下文与嵌套事务章节;动态文档,未锁发布版本,未安装或运行该驱动。
- SQLite:Transaction。页面标注更新于2026年2月18日;实际阅读事务与错误响应章节。用于提醒错误影响依类型而异,不将所有SQL错误等同于整单自动回滚。

留下你的想法 0
欢迎提问、补充和不同观点。评论通过审核后会公开显示。