TT Lab
开始
学习 学习路径 课程

AI 智能体 — 是图不是模型

大额退款连审批队列都没进

在 TT Lab 中继续学习

目标

在无法撤销的事情面前让图停下,并在人修改数值或拒绝之后接着运行。把两种停下的方法都做一遍,并用亲自数出的数字确认两者的差别。

为什么重要

在 LangGraph 中,暂停是建立在保存之上的。暂停的意思是“不在现在执行下一步,而是在留下的状态上,稍后接着做”,所以没有留存的地方(checkpointer),就无法接着做。然而只给断点而没有 checkpointer,也不会报错——图会悄悄停住,返回只有一半的状态。在本实验中要先亲眼看到这一点。 暂停之后人做的事不只是批准。有时会砍低金额,有时会拒绝。用 update_state 修改的值会经过那个键的 Reducer 再写入,所以覆盖型的键和接续拼接型的键的行为不同。 停下的方法有两种。interrupt_before 在进入节点之前停下,节点内的 interrupt() 则在调用的那个位置停下。后者在恢复时该节点会从头再运行——这意味着放在前面的副作用会发生两次。本实验中亲自数一数进入了几次。 评分器不会相信你写下的说明。它会真正导入你的模块,用任意的金额和原因来运行,并把停下的位置和接着运行的结果与评分器另外计算的值对照。

步骤

  1. 在 /root/work/aghitl/approve.py 中创建 AUTO_LIMIT = 100000、RISKY、needs_approval(amount, reason)、State、三个节点(intake、auto_settle、settle)、triage 和 build_graph()。此时还不停下。
  2. 增加 build_forgotten() 和 run_forgotten(amount, reason)。运行没有 checkpointer 而只给了 interrupt_before=["settle"] 的版本,记下会发生什么。
  3. 增加 SAVER = MemorySaver()、build_pausing()、thread(name)、start_case(amount, reason, name),让它真正停下。start_case 返回 {"next": ["settle"], "outcome": "", "paid": -1}。
  4. 增加 resume_case(name),用 invoke(None, config) 接着运行。
  5. 用 resume_case(name, amount=..., decision=...),让它在人修改金额或拒绝之后接着运行。拒绝的话,一分钱也不会付出去。
  6. 增加 confirm 节点、build_asking() 和 ask_and_answer(amount, reason, answer, name)。在节点内部用 interrupt() 停下,再用 Command(resume=답)(占位符为人给出的答案)接着运行。用 SIDE_EFFECTS 统计进入了节点几次。
  7. 增加 handle(...),把一条处理到最后,并用一行留下请求值、执行值和是否批准。
  8. 在 /root/work/aghitl/approve_report.json 和 /root/work/aghitl/approve_report.md 中留下记录。

参考

区分哪些事要人看

在 /root/work/aghitl/approve.py 中创建 AUTO_LIMIT = 100000、RISKY、needs_approval、State、三个节点、triage 和 build_graph()。小额且原因简单就走 auto_settle,否则走 settle。此时还不停下。

把标准写在 needs_approval 一处,修改策略就变成修改这个函数。triage 返回分支名称(auto、hold),通向哪个节点由路径映射决定。节点名称要取得与状态键不同。

漏掉 checkpointer 就会悄悄消失

增加 build_forgotten() 和 run_forgotten(amount, reason)。运行没有 checkpointer、只给了 interrupt_before=["settle"] 的版本,把是否抛了异常、走到了哪里、能否接着运行,记为 {"raised": ..., "trace": [...], "outcome": ..., "resumable": ...}。

自己运行一下,按看到的写下来。结果会与预期不同——这种意外就是这一步的全部意义。因为没有保存任何东西,所以也无法用 invoke(None, ...) 接着运行。它之所以危险,就在于日志里一个错误也没有。

真正停下

增加 SAVER = MemorySaver()、build_pausing()、thread(name)、start_case(amount, reason, name)。需要审批的条目在 settle 之前停下,start_case 返回 {"next": ["settle"], "outcome": "", "paid": -1}。

三样东西必须同时具备——checkpointer、thread_id、中断点。get_state(config).next 不为空,就意味着“还没结束”。小额的条目不会停下,而是直接结束,所以重点是存在一个不经过设有中断点的节点的分支。

从留下的地方接着运行

增加 resume_case(name),用 invoke(None, config) 接着运行。答案是 {"outcome": "settled", "paid": 금액, "trace": [...]}(占位符为金额)。

在输入位置传入 None,意思就是“没有新输入。从保存的地方接着做”。如果再次放入原来的输入,就会从头再运行,足迹变成两倍,一眼就能看出来。必须使用同一个 thread_id 才能接上。

审批人修改金额或拒绝

让 resume_case(name, amount=..., decision=...) 先用 update_state 修改状态,再接着运行。decision="reject" 时,outcome 为 rejected,paid 为 0。修改的痕迹以 human 留在 trace 里。

update_state 放入的值也要经过那个键的 Reducer。像 amount 这样的覆盖型键会被覆盖,而像 trace 这样的接续拼接型键会累积——所以很适合留下一行痕迹。拒绝也必须是一种结果。如果让审批人以“干脆不点”来拒绝,那一条就会永远留在队列里。

在节点内停下,该节点会运行两次

增加 confirm 节点、build_asking() 和 ask_and_answer(amount, reason, answer, name)。用 interrupt() 停下,用 Command(resume=답)(占位符为人给出的答案)接着运行,并用 SIDE_EFFECTS 统计进入了节点几次。

interrupt() 返回的值就是人给出的答案。恢复后该节点会从头再运行,所以放在 interrupt() 之前的事会发生两次——请亲自数一数来确认。停下时传出的值放在 get_state(config).tasks 里的 interrupts 中。

把请求的值和实际出去的值一起留下

增加 handle(amount, reason, name, amount_override=None, decision=None),把一条处理到最后,并返回 {"approval": ..., "outcome": ..., "paid": ..., "requested": ..., "changed_by_human": ...}。

审批记录的核心不是人的名字,而是变化的值。如果请求的金额与实际付出的金额不同,这一事实必须写在记录里,以后才能解释。不需要审批的条目记为 not_required,并且不停下。

记录看到了什么

在 /root/work/aghitl/approve_report.json 中写入 auto_limit、forgotten、small、large、trimmed、rejected、confirm_entered,在 /root/work/aghitl/approve_report.md 中用 ## 어디서 멈추기로 했나(韩文,意为“决定在哪里停下”)、## 체크포인터를 빠뜨리면(韩文,意为“漏掉 checkpointer 会怎样”)、## 사람이 고친 값은 어디로 들어가나(韩文,意为“人修改的值进入哪里”)、## 노드 밖에서 멈추기와 노드 안에서 멈추기(韩文,意为“在节点外停下与在节点内停下”)四节来写。

forgotten 原样放入 run_forgotten 的答案,small、large、trimmed、rejected 只放入 handle 的答案中的 approval、outcome、paid 三个键。confirm_entered 是 {"before": 정수, "after": 정수}(占位符为整数),是 ask_and_answer 统计出的值。不要编造,要运行后得到。