智能体一放手就跑偏,写死流程又太僵,分界线到底划在哪
数据截至 2026-07。本文只讲编排层的划线方法,不涉及任何具体产品的额度、配置项与报错口径;各框架和命令行工具的接口以官方最新说明为准。
大多数「这个 agent 越用越不敢交给它」的问题,根因不是模型不够聪明,也不是那段指令写得不够细,而是你把一个本来只有唯一正确解的步骤,交给了模型去「决定」。 一旦某一步存在唯一正解,模型的自由度就只能带来负收益:它有概率选对,也有概率选错,而你为这点自由度付出的代价是每次运行都要重新验证一遍。反过来也成立——你把一个本该由模型现场判断的步骤写成了固定分支,它就会在你没预料到的输入上硬撞过去,撞出来的东西比跑偏更难查,因为流程日志看上去一切正常。
先说清楚这篇和站内另外两篇的分工:ReAct 智能体的原理 讲的是「思考—行动—观察」这个循环本身怎么转,智能体的角色分工怎么设计 讲的是多个角色之间怎么切分职责。这篇不重复那两件事,只回答一个更靠前的问题:在你已经决定要做一条流水线之后,哪些环节应该由代码写死顺序,哪些环节应该留给模型现场决定,以及当它出问题时按什么顺序排查。
一、先分清你遇到的是哪一类不稳定
「跑偏」是个笼统的说法。同样一句抱怨底下藏着三种完全不同的故障,处置动作也完全不同。把它们分开是排查的第一步,也是最容易被跳过的一步。
决策漂移:同一个输入,两次运行选择了不同的动作序列。第一次它先读配置再改代码,第二次它直接改代码然后去跑测试。序列不同,结果可能都对,也可能一次对一次错。
执行漂移:动作序列稳定,但每一步产出的东西质量在抖。同样是「写一个校验函数」,这次写得很完整,下次少了边界处理。序列没变,内容变了。
环境漂移:动作和产出都没问题,但外部世界在它跑的过程中变了。分支被切走了、依赖装上了新版本、上一轮留下的临时文件还在。这种最阴,因为复跑一次可能就好了,你会误以为是模型的随机性。
判别方法很朴素:固定输入连跑三次,把每次实际执行的动作序列记下来,只记动作名和关键参数,不记自然语言。
# 每次复跑前先确认三次的起点一模一样,否则你比的是三个不同的环境
git rev-parse --short HEAD # 起点提交是否相同
git status --porcelain # 工作区是否残留上一轮的改动,有输出就不是干净起点
git stash list # 有没有忘记弹出的暂存,它会让人误判基线
三次的动作序列一致,说明决策是稳的,问题在执行层;三次序列各不相同,说明你把决策权交出去的那个点位选错了;三次都一致且产出也一致,但结果时对时错,那就去查环境。
二、一张判别表:现象到动作
| 现象 | 大概率成因 | 怎么验证 | 处置动作 |
|---|---|---|---|
| 同一输入,三次跑出三种动作顺序 | 决策点开得太宽,把有唯一正解的步骤交给了模型 | 把三次动作序列并排对比,看分歧出现在第几步 | 把分歧点之前的部分用代码固定成骨架,只留分歧点之后的空位 |
| 动作顺序稳定,但产出质量忽好忽坏 | 执行层缺少验收条件,模型不知道什么叫做完 | 给同一步骤加一条可机器判定的通过条件,再跑三次看通过率 | 在这一步出口加校验,不通过就重试或退回 |
| 前两轮都对,第三轮开始乱 | 上下文里堆了太多中间产物,早期约束被稀释 | 把这一步单独拎出来用干净上下文跑一次,看是否正常 | 分段执行,每段结束把中间结果落成文件,下一段只读文件 |
| 它声称做完了,实际文件没动 | 缺少事后核验,模型的自述被当成了结果 | 用 git status --porcelain 或文件时间戳核对 | 把验收从模型自述改成外部检查 |
| 工具调对了但参数总差一点 | 工具接口描述有歧义,或参数存在隐含依赖 | 把这次调用的参数原样贴出来,人工判定对错 | 收窄参数类型,能枚举就别开放自由文本 |
| 中途卡住不动,也不报错 | 某一步在等一个永远不会来的输入 | 看最后一条动作是什么类型,是否需要交互 | 编排层加两个硬上限:总步数上限、单步等待超时,触顶就中止并转人工 |
| 每次都对,但换个项目就全崩 | 流程写得太死,把项目特有假设编进了骨架 | 换一个结构不同的仓库跑一遍 | 把项目相关的部分从骨架里抽出来变成输入 |
这张表的用法是从左往右走,不要跳过「怎么验证」那一列。绝大多数返工都来自跳过验证直接改,改完现象消失了,其实只是概率变了。
三、划线的四条判据
判断某一步该写死还是该放手,我用四条判据打分,每条计 0 或 1 分。
第一条,这一步是否有唯一正解。 提交前跑一次测试、改完代码格式化、发布前打 tag,这些动作没有第二种正确做法。有唯一正解,计 1 分。
第二条,结果是否可机器验证。 能用退出码、schema 校验、文件存在性判定对错的,计 1 分。只能靠人读一遍才知道好不好的,计 0 分。注意这条不是问「难不难验证」,是问「能不能不靠人」。
第三条,可选分支是否有限且已知。 分支能被枚举出来(三选一、五选一),计 1 分;分支空间开放(写一段什么样的文案)计 0 分。
第四条,做错的代价是否不可逆。 删文件、推远端、动数据库、发消息给真实用户,这些做错了收不回来,计 1 分。改本地文件、生成草稿这类随时能撤销的,计 0 分。
四条加起来:
- 得 3 分或 4 分:写死。用代码控制顺序和参数,模型只负责填内容,不负责决定要不要做、什么时候做。
- 得 0 分或 1 分:放手。让模型自己决定,你在出口加一层校验就行。
- 得 2 分:受限选择。这是最容易做错的一档,后面单说。
举个具体的。「改完代码要不要跑测试」——唯一正解(要跑)、可机器验证(退出码)、分支有限(跑或不跑)、代价可逆(跑一次而已,不计分),三分,写死。「这段业务逻辑该拆成几个函数」——没有唯一正解、不可机器验证、分支开放、可逆,零分,放手。「该改哪几个文件」——对一个描述清楚的缺陷来说,正确的文件集合是唯一的(1 分),改完能靠测试反过来验证(1 分),但候选范围是整个仓库、没法枚举(0 分),改本地文件也随时能撤(0 分),合计两分,落在受限选择里:你先用代码把候选文件列出来,让模型从这个列表里选,而不是让它自己去满仓库找。这一步的收益在大仓库里非常明显,因为满仓库找文件本身就是决策分支最多、最容易失手的环节。
四、三种落地形态
判据落到代码里,只有三种形态。
骨架写死加空位。 用普通代码写出步骤顺序,每个步骤内部留一个由模型填的空位。骨架里不要出现任何「如果模型觉得需要就……」的逻辑,那等于把决策权又还回去了。
steps = ["locate", "patch", "verify", "summarize"]
for name in steps:
result = run_step(name, ctx) # 顺序由代码决定
ctx = check_and_merge(name, result) # 每步都有出口校验
受限选择。 让模型输出一个结构化的选择而不是一段散文,然后在代码里校验它是否落在允许集合内。落不进去就重试一次,再落不进去就转人工。
import json
ALLOWED = {"patch", "revert", "escalate"}
def parse_choice(raw: str) -> str:
start, end = raw.find("{"), raw.rfind("}")
if start < 0 or end < 0:
raise ValueError("output contains no json object")
data = json.loads(raw[start:end + 1])
action = data.get("action")
if action not in ALLOWED:
raise ValueError(f"unexpected action: {action!r}")
return action
这段代码看着简单,作用是把一整类「它自由发挥了一个不存在的动作」的故障挡在编排层之外。结构化输出本身也会不稳,那属于另一类问题,不要和编排层的决策边界混在一起排查。
自由发挥加出口闸。 只在零到一分的步骤上用。出口闸不是让模型自己检查自己,是外部条件:测试通过、schema 校验通过、diff 行数在阈值内、没有触碰指定目录之外的文件。最后一条尤其重要,改动范围失控是自由发挥最常见的翻车方式。
# 出口闸的一种朴素实现:改动范围超出预期就停下来人工看
git diff --stat
# 用 status 而不是 diff:已暂存的改动和新建的未跟踪文件,git diff 默认都看不见
# porcelain 每行前 3 个字符是状态位,cut -c4- 取出路径
git status --porcelain | cut -c4- | grep -v '^src/' \
&& echo "有 src/ 之外的改动,先人工确认再继续"
对不可逆动作,出口闸应该是人。把人放在哪个位置、放几个,人在环路怎么设计 那篇讲得更细,这里只强调一点:人工确认点要放在不可逆动作的正前方,不是流程末尾。末尾确认等于事后追认,没有拦截价值。
五、什么时候别再折腾
调编排最大的浪费不是方向错,是明明该停却还在加规则。给自己定几条硬线。
止损点一:同一个失败点连续三轮改不动。 第一轮你补了一条约束,第二轮你换了个描述,第三轮你把步骤拆细了,现象还在。到这里就别再改指令了,改架构:把这一步从「模型决定」降级成「代码决定」或「人决定」。三轮是经验值,重点不在数字,在于你要提前把这个数字定下来,不然会一直改下去。
止损点二:规则条数开始超过步骤条数。 当你为了约束一个五步流程写了二十条注意事项,说明这条流程的决策点开错了位置。正确的做法是把其中若干条规则变成代码里的判断,规则文件只保留那些确实需要模型理解的语义约束。判断某条规则该留在文字里还是该下沉到代码,看它能不能被写成一个布尔表达式。
止损点三:单次运行的时间明显长于你自己做。 这时候它已经不是效率工具了。先看是不是陷入了循环——同一个动作带着几乎相同的参数反复出现,就是循环;如果不是循环,那就是任务本身不适合交出去。
回滚点怎么留。 在放手的步骤之前留一个可以无脑退回的位置,这是整套做法能跑起来的前提。
git switch -c agent/attempt-3 # 每次尝试独立分支
git add -A && git commit -m "baseline before free-form step" # 先把当前状态定成基线
# 出问题时:先把这一轮跑出来的改动清干净,再切回去删分支
git reset --hard HEAD # 退回本分支的基线提交
git clean -fd # 清掉新建的未跟踪文件
git switch -
git branch -D agent/attempt-3
两个细节容易翻车。一是清理必须在切回去之前做:未提交的改动不会被分支挡住,git switch 会把它们原样带回原分支,你以为删掉了分支就干净了,其实脏东西还在工作区。二是基线提交要用 git add -A 而不是 git commit -a,后者不收录新建的未跟踪文件,等你 reset --hard 时那些文件也退不回去。如果切分支时工作区本来就是干净的,那条 commit 会提示没有可提交内容,跳过即可。
分支比 stash 可靠,因为 stash 在多轮尝试后很容易搞混谁是谁。如果流程会动到工作区之外的东西——数据库、缓存、消息队列——那这些东西必须有独立的回滚手段,没有就别放手,直接降到人工确认。
换条路的判断依据。 有三种情况建议直接放弃编排、改成人工加脚本:任务的验收条件你自己都说不清楚;每次输入的结构都不一样,没有可复用的骨架;出错的代价大于省下的时间。第三条最容易被忽略,算一下就知道值不值。
六、避坑清单
坑一:把「模型能做」当成「应该让模型做」。 会踩是因为验证的时候它确实做对了,于是你顺手就留在了流程里。避法是回到四条判据打一遍分,得 3 分以上的步骤,不管它做得多对都要写死——你要的不是这一次对,是每一次都对。
坑二:在骨架里写「如果需要就跳过这一步」。 会踩是因为你想让流程更聪明一点,省掉无用功。结果是决策权被悄悄还了回去,而且这次的还权不在任何日志里。避法是让跳过的条件成为代码可判定的表达式,比如「目标文件的哈希没变则跳过」,不是「如果你觉得没必要就跳过」。
坑三:出口校验用模型自己来做。 会踩是因为写起来最快,加一句「检查一下你刚才的输出」就完事。问题在于它的自查和它的产出来自同一个上下文,错误往往是一致的。避法是外部条件判定,实在需要模型参与,也要换一个干净的上下文重新读产物。
坑四:把重试当成修复。 会踩是因为重试一次经常就好了,看上去问题解决了。真实情况是你把一个必现故障变成了偶发故障,以后更难查。避法是重试必须带计数和分类,重试成功也要记一笔,这样你能看出哪一步的重试率在涨。这和前面判别表里的「不通过就重试或退回」并不冲突:重试是允许的止血手段,但只允许用在瞬时性失败上(超时、限流、临时连不上),而且必须有次数上限;只要同一个输入连着几轮都落在同一个失败点,那就是必现故障,再重试多少次都是同一个结果,该做的是把这一步的决策点挪位置。重试策略的细节见 失败重试怎么做。
坑五:所有步骤共用一个上下文。 会踩是因为省事,而且前几步确实靠共享上下文才连得起来。代价是越到后面,早期的约束越容易被中间产物挤掉。避法是分段,段与段之间只传结构化的中间结果,不传对话历史。中间结果落成文件还有个附带好处:出问题时你能直接打开看,不用翻整段对话。
坑六:把人工确认点放在流程末尾。 会踩是因为末尾确认写起来最简单,一个开关就行。但不可逆动作已经发生了,你确认的只是既成事实。避法是在不可逆动作前面单独设卡,其余地方全部放行——卡设得越少越靠前,人才会认真看。
坑七:拿一次成功的运行当作流程稳定。 会踩是因为演示的时候确实很顺。避法是固定输入连跑三次以上再下结论,判断标准不是「结果对不对」,是「动作序列是否一致」。序列一致才叫稳定,结果对只是这次运气好。
坑八:调试时同时改两处。 会踩是因为你觉得两处都有问题,一起改省时间。结果是现象消失了你也不知道是哪一处起的作用,下次复发只能从头来。避法是一次只动一处,动完复跑三次。框架层面的调试次序,智能体框架怎么调试 讲得比这里细。
收束
这条界线其实不玄:一步动作如果有唯一正解、能被机器验证、分支可枚举、做错了收不回来,就把它写进代码;反过来就交给模型,然后在出口挂一道外部校验。真正难的不是划线,是承认自己划错了之后老老实实把决策点挪位置,而不是再补三条规则。
开工前过一遍自检:
- 这条流程里,哪几步得分在 3 分以上?它们现在是由代码控制顺序的吗?
- 每个放手的步骤,出口校验是外部条件还是模型自述?
- 不可逆动作前面有没有单独的人工卡点?
- 有没有一个能无脑退回的回滚点,包括工作区之外的状态?
- 固定输入连跑三次,动作序列一致吗?
- 你的止损轮次定成几轮,写下来了吗?
前四条答不上来,就别急着扩大它的权限范围;后两条答不上来,说明你现在对它的稳定性还只是印象,不是判断。