把上限调高,那一单还是跑不完
目标
亲手做一个带循环的图,亲手测量递归上限数的到底是什么,并把触及上限和完成了任务分别记成不同的结果。最后做一个诊断,筛出够不到结束条件的输入。
为什么重要
在 LangGraph 中,循环不是什么特殊语法,只是往回走的边。所以做起来容易,让它结束却很难。即使有结束条件,只要混进一个无论如何也达不到该条件的输入,那一条就会永远转下去——本实验的智能体正是如此。扣分的原因有两个(违禁词、长度),而修订节点只修其中一个。
递归上限在这种时候是最后一道防线,但如果不知道它数的是什么,就会误诊。上限数的不是节点执行次数,而是超级步,并排放置的节点不论多少个,都只算一个超级步。本实验中会亲手测量来确认。
而且,触及上限而抛出异常,与图自己结束,是完全不同的两件事。如果都写进同一格“失败”,以后就分不清问题出在哪里。把上限设为两层(状态中的 rounds 和 recursion_limit),并让内层上限留下结果。
评分器不会相信你写下的说明。它会真正导入你的模块,用任意句子检验规则函数,用任意上限运行图,并把得到的结果与评分器另外计算的值对照。
步骤
- 在 /root/work/agroute/route.py 中创建
BANNED、MIN_LEN、PASS、make_draft、score_of、revise_text、State、四个节点(compose、review、revise、ship)、after_review和build_graph()。在从review分出的条件边上给出路径映射,这一步中revise之后是 END。 - 加上从
revise回到review的边,构成循环。含有多个违禁词的询问,必须转几圈后通过。 - 增加
build_unbounded()和run_unbounded(question, limit)。用给定的上限运行故意去掉结束条件的版本,触及上限时返回{"error": "GraphRecursionError", "limit": 한도, "message": 예외 메시지}(占位符依次为上限值与异常信息)。 - 增加
MAX_ROUNDS = 5和escalate节点,让build_graph()对任何输入都能不抛异常地结束。触及上限时outcome为escalate。 - 增加
chain_graph(n)、fan_graph(n)、min_limit(app, state)。min_limit要找出把该图运行到结束所需的最小recursion_limit。 - 增加
run_guarded(question, limit),用不同的status记录已完成的和触及上限的。 - 增加
diagnose(question),筛出这个输入是否能满足规则,不能的话原因是什么(too_short或banned)。 - 在 /root/work/agroute/route_report.json 和 /root/work/agroute/route_report.md 中记录测得的数字和判断。
参考
- 执行契约:评分器会把
/root/work/agroute/route.py当作 Python 模块导入,直接使用上面列出的名称。不会作为脚本运行。 make_draft(question)是question.strip() + " 라고 안내드립니다."(韩文,意为“特此说明”)。score_of(text)从 100 分开始,每出现一个违禁词扣 30 分,长度不足MIN_LEN则扣 40 分,且不会降到 0 以下。revise_text(text)只删除一个违禁词。- 常量:
BANNED = ("무조건", "절대로", "보장합니다")(韩文,意为“无论如何”“绝对”“保证”),MIN_LEN = 30,PASS = 70,MAX_ROUNDS = 5。 - 状态键:
question、draft、score、rounds、outcome、trace。trace使用接续拼接的 Reducer。 - 节点名称和状态键不能重名。LangGraph 把二者放在同一个命名空间里,所以如果把写草稿的节点叫
draft,编译时会出现ValueError: 'draft' is already being used as a state key。因此本实验的第一个节点名叫compose。 after_review返回的是分支名称(ship、revise、escalate)。哪个分支通向哪个节点,由add_conditional_edges的第三个参数决定。chain_graph(n)和fan_graph(n)返回有 n 个节点的图。节点名称随意,每个节点向一个接续拼接的键写入自己的名称。min_limit(app, state)从 1 开始逐步调高并尝试invoke,返回第一次成功的上限。失败会以langgraph.errors.GraphRecursionError的形式出现。run_guarded的答案:完成时为{"status": "done", "rounds": 정수, "outcome": "ship"|"escalate", "score": 정수},触及上限时为{"status": "limit", "rounds": -1, "outcome": "", "score": -1}(占位符为整数)。diagnose的答案:{"fixable": 참거짓, "reason": ""|"too_short"|"banned", "best_score": 정수, "rounds_needed": 정수}(占位符依次为布尔值、整数、整数)。best_score是把违禁词删到不能再删之后的分数。- 这个 Pod 没有互联网。langgraph 0.2.60 已经装好了。
- 官方文档:Use the graph API · GRAPH_RECURSION_LIMIT · Graph API overview
- 常见错误:让条件函数返回节点名称而不是分支名称并省略路径映射、把结束判断放在节点和条件函数两处、靠调高上限来掩盖症状、以为并排放置的节点会消耗上限。
区分分支名称与路径映射
在 /root/work/agroute/route.py 中创建三个规则函数、四个节点、after_review 和 build_graph()。给 review 的条件边提供路径映射 {"ship": "ship", "revise": "revise"},这一步中 revise 之后是 END。
条件函数接收状态,返回分支名称。如果让它直接返回节点名称,每次改图的形状都得修改判断代码。score_of 如何扣分是后面步骤的材料,所以请严格遵守说明中的规则。
把修订过的再送去评分
加上从 revise 回到 review 的边,构成循环。有三个违禁词的询问,必须转三圈后流向 ship。
循环不是特殊语法,只是往回走的边。圈数取决于 revise 一次只删一个违禁词这一点。统计圈数的 rounds 由 revise 来加——这样以后才有地方设置内层上限。
亲眼看到触及上限
增加 build_unbounded() 和 run_unbounded(question, limit)。用给定的上限运行故意去掉结束条件的版本,触及上限时返回 {"error": "GraphRecursionError", "limit": 한도, "message": 예외 메시지}(占位符依次为上限值与异常信息),正常结束时把 error 设为空字符串。
异常是 langgraph.errors.GraphRecursionError。消息里直接包含上限数字,所以不要编造,把 str(예외)(占位符为异常对象)原样放进去。build_unbounded() 必须是与 build_graph() 不同的图——因为即使后面的步骤给 build_graph() 加上了上限,这一步也必须继续有效。
留下结果的内层上限
增加 MAX_ROUNDS = 5 和 escalate 节点,并让 after_review 在达到上限时返回 escalate 分支。路径映射中也要加上该分支。现在 build_graph() 对任何输入都必须不抛异常地结束。
触及外层上限(recursion_limit)会抛异常,结果就消失了。内层上限会留下结果——这就是这一步的要点。判断只放在 after_review 一处。如果在节点里也判断是否结束,总有一天两处会不一致。
上限数的是超级步
增加 chain_graph(n)(串联)、fan_graph(n)(并排)和 min_limit(app, state)。min_limit 要找出并返回把该图运行到结束所需的最小 recursion_limit。
从 1 开始逐步调高,每次 invoke,返回第一次成功的值即可。两个图的答案有何不同,是这一步的关键——并排放置的节点不论多少个,都在同一个超级步里运行。状态里有一个加了 Reducer 的键就够了。
区分已完成与触及上限
增加 run_guarded(question, limit=25)。图自己结束时返回 {"status": "done", "rounds": 정수, "outcome": "ship"|"escalate", "score": 정수},触及上限时返回 {"status": "limit", "rounds": -1, "outcome": "", "score": -1}(占位符为整数)。
把两件事写进同一格“失败”,以后就无法区分。所做的只是捕获异常并换一个名称,但这一行在生产环境中差别很大。上限给得小的话,正常输入也会得到 limit——评分器会检验这一点。
筛出修了也不会变好的输入
增加 diagnose(question),让它返回 {"fixable": 참거짓, "reason": ""|"too_short"|"banned", "best_score": 정수, "rounds_needed": 정수}(占位符依次为布尔值、整数、整数)。best_score 是把违禁词删到不能再删之后的分数。
不运行图也能知道——把 revise_text 一直应用到不再变化,再测量那时的分数即可。如果该分数达不到 PASS,这个输入无论转多少圈都通不过。诊断比调高上限便宜,原因就在这里。
用测得的数字汇报
在 /root/work/agroute/route_report.json 中写入 default_recursion_limit、chain_min_limit、fan_min_limit、clean、dirty、stuck、stuck_reason,在 /root/work/agroute/route_report.md 中用 ## 고리를 어디에 두었나(韩文,意为“把循环放在了哪里”)、## 재귀 한도는 무엇을 세는가(韩文,意为“递归上限数的是什么”)、## 상한에 닿은 것과 마친 것(韩文,意为“触及上限的与已完成的”)、## 고쳐도 나아지지 않는 입력(韩文,意为“修了也不会变好的输入”)四节来写。
default_recursion_limit 是不提供 config 时的默认值——不要编造,要一边调高上限一边测量来确认。chain_min_limit 和 fan_min_limit 是以节点数为字符串键的对象(例如 {"1": ..., "2": ..., "3": ...})。clean、dirty、stuck 分别原样放入 run_guarded 的答案。