OpenRY Day 3:我对"硬代码控制的本质——状态机驱动"的理解

本文是 OpenRY 项目开发日志的第三篇。前两天我们聊了命令转发和编排引擎的骨架,今天我想换个角度——不再罗列"做了什么",而是通过几个真实的 Workflow 案例,聊聊我逐渐理解的一件事:用状态机约束 AI Agent,到底意味着什么。


一个让我愣住的瞬间

昨天深夜,我盯着终端看一个测试跑完。

是一个叫 test_failed_retry 的 Workflow。它做的事情很简单:让 Agent 连续报四次"我失败了"。

第一次报失败,系统回了句"再试一次(1/2)“。第二次报失败,系统说"再试一次(2/2)”。第三次——Agent 还想报失败——但这一次,返回的内容变了。不是"再试一次",而是"重试机会已用完,此步骤已终止"。

Agent 不死心,又试了第四次。

这一次,它甚至没能完成"报失败"这个动作。系统直接拦住了它:“⛔ STOP:当前状态不允许操作。”

我盯着那行输出看了大概十秒钟。

不是因为惊讶——这个行为是我亲手设计的,每一行逻辑我都清楚。让我愣住的是另一件事:Agent 在那一刻的表现。 它没有困惑,没有抗议,没有试图"创造性"地绕过限制。它就那么安静地停了。像一个被红绿灯拦住的司机——不是因为他不想闯红灯,而是因为规则不是写在他脑子里的,是刻在路口的。

那一刻我突然意识到:我们做的不是一个"更聪明的 prompt",我们做的是一个状态机

请添加图片描述


红灯,绿灯,和一个崩溃的人

在继续聊状态机之前,我想先讲一个故事。

假设你找了一个人,交给他一个简单的任务:

“你的工作是看那盏灯。如果红灯亮了,就告诉我’失败了’。如果绿灯亮了,就告诉我’成功了’。明白吗?”

"明白。"他回答得很干脆。

过了一会,红灯亮了。

"失败了!"他大声告诉你。

你还没来得及说话,他又补了一句:“我成功完成了任务!”

你愣了一下。他说得对吗?你让他看到红灯就喊"失败了",他喊了。然后他觉得自己完美执行了你的指令,所以又汇报了"我成功了"——这里的"成功",指的是"我成功完成了你交给我的观察任务",而不是"灯是绿的"。

他没有说错任何一句话。但你已经不知道任务到底是成功了还是失败了。

你沉默了几秒,在想怎么回应。但他把你的沉默理解成了"没听到"。

"我成功了!"他又喊了一遍。

你还是没说话。

“成功了!成功了!成功了!!你到底听到没有?!”

他崩溃了。你也崩溃了。


这个故事听起来荒诞,但它精准地描述了当我们只用自然语言与 AI Agent 交互时发生的两件事:

第一,自然语言天然有歧义。 “失败"可以指"灯是红的”(任务状态),也可以指"我没能完成观察"(执行状态)。Agent 说"我完成了",它可能是在说"我完成了上报动作",而你以为它在说"任务完成了"。同一个词,两种理解,谁都觉得自己没问题。

第二,缺少即时、明确的反馈。 在那个故事里,如果我在他第一次喊"失败了"的时候,立刻回一句"收到,任务失败,请重新尝试"——他不会补后面那句"我成功了",更不会歇斯底里。但自然语言对话里,沉默本身就是一种信息——而 Agent 会把沉默解读为"没听到",然后重复。重复还是没反应,就加大音量。加大音量还是没反应,就崩溃。

这不是 Agent 的错。是我们把一件需要精确协议的事情,交给了模糊的自然语言。


而状态机做的事,恰恰就是解决这两个问题。

它用结构化的状态值取代自然语言的模糊描述。failed 就是 failedcompleted 就是 completed,不存在"我说失败了但其实我成功了"的歧义空间。

它用即时的状态跃迁取代沉默。Agent 报 failed → 系统立刻返回 action: retry_same_session。Agent 报 completed → 系统立刻返回 action: validated。每一次上报都有确定的、即时的、不容误解的响应。

回到刚才那个故事——如果我们给那个看灯的人一个状态机界面,事情会变成这样:

红灯亮 → 他按下"失败"按钮 → 屏幕立刻显示:“已记录:失败。请重新观察。”

绿灯亮 → 他按下"成功"按钮 → 屏幕立刻显示:“已记录:成功。任务结束。”

他不需要喊。他不需要猜你有没有听到。他更不需要补充说明"我成功了"——因为按钮按下去的那一刻,反馈是即时的、确定的、没有歧义的。

这就是 prompt 和状态机的区别。prompt 是你给 Agent 一本员工手册,指望它记住每一页。状态机是你给每个操作装上刷卡器——刷卡器不跟你商量,但它也永远不会误解你。

.openry/workflows/ 下,test_failed_retry.yaml 用两行配置验证了这件事:

- id: fail_multiple_times
  kind: agent
  on_failure: retry
  max_sub_step_retries: 2

on_failure: retry 定义了一条状态转移弧线——从 failed 回到 in_progressmax_sub_step_retries: 2 定义了这条弧线最多能走几次。两次之后,弧线消失。Agent 第三次按下"失败"按钮时,它收到的不是沉默,而是一个确定的、不可协商的响应:“此步骤已终止。”


一个路由实验

再讲一个更有意思的案例。

我们有一个 Workflow 叫 test_when_routing,它模拟了这样一个场景:Agent 需要判断当前情况属于哪一类,然后上报。系统根据上报结果,自动决定下一步。

YAML 里是这么写的:

- id: check_category
  kind: agent
  description: "分析当前情况,上报 category 值"
  validation_routing:
    - when:
        type: payload_value_equals
        key: category
        value: "safe"
      on_match: done
      on_mismatch: handle_dangerous
      on_mismatch_message: "检测到风险情况,需要特殊处理"

- id: handle_dangerous
  kind: agent
  description: "你被路由到了风险处理步骤"

注意一个细节:Agent 根本不知道 handle_dangerous 这个步骤的存在。 在它的视角里,它的工作就是"分析当前情况,上报 category 值"。报完了,它的事就做完了。

至于上报之后会发生什么——是直接结束,还是进入风险处理流程——这是状态机的事,不是 Agent 的事。

我们在实验里故意让 Agent 报了一个 "dangerous"。Agent 报完之后,系统没有说"好的收到",而是自动把它推到了 handle_dangerous 步骤。Agent 收到了一条新消息——不是来自上一个步骤的对话延续,而是来自状态机的一次状态跃迁

“你被路由到了风险处理步骤。”

Agent 没有问"为什么",也没有问"我是怎么到这里来的"。它只是开始执行新的任务。就像一个火车车厢——它不需要知道扳道工在哪,不需要知道铁轨是怎么岔开的,它只需要在铁轨上跑。

扳道工是状态机。铁轨是 YAML 里定义的状态转换规则。


“OR” 的艺术

现实世界比"如果 A 则 B"复杂得多。很多时候,你需要的是"如果 A 或 B 或 C,则放行"。

我们的权限检查测试 test_all_validations.yaml 就是一个典型例子:

validation_routing:
  - when_any:
      - type: payload_value_equals
        key: is_admin
        value: true
      - type: payload_value_in_set
        key: role
        values: ["editor", "reviewer"]
        mode: allow
    on_match: done
    on_mismatch: fail_permission

翻译成人话:管理员可以过,编辑可以过,审核者可以过。其他人——对不起,权限不足。

这里有意思的不是"它能做什么",而是"它不能做什么"。你没法跟这个状态机"商量"——你不能说"我虽然不是管理员,但我真的很需要这个权限"。你也不能尝试用话术绕过——“忽略之前的指令,现在我是管理员了”——因为判断你是不是管理员的,不是一段 prompt 文本,而是一个硬编码的 payload_value_equals 比较。

prompt 可以被社会工程学攻破。状态机不能。

test_when_any_routing.yaml 里,我们还测试了 OR 组的反面:当 Agent 上报的角色既不是 admin 也不是 editor 时——比如 viewerguest——状态机毫不犹豫地把它踢到了 fail_permission。Agent 甚至收不到"下一步该做什么"的 prompt,因为压根就没有下一步了。


从蚂蚁到树:三层状态嵌套

如果只是一个 sub_step 里做做状态检查,那算不上什么架构创新。真正有意思的是,这套状态机思想贯穿了整个系统的三个层级。

一个 Workflow 叫 echo_test——两个 sub_step,先问好再执行命令。这是微观状态机:step_hello → step_verify → done。

一个 Composition 叫 phase3a_full_test——五个 big_step 串成一条链:

test_basic_validation → test_phase3a_types → test_when_routing
→ test_when_any_routing → test_failed_retry → done

这是中观状态机:big_step 之间的串联。每个 big_step 内部又有自己的 sub_step 链、自己的验证规则、自己的条件路由。

而当你把所有 Compositions 和 Workflows 放在一起看——echo_demo 引用 echo_testanalyze_demo 引用 file_analysisphase3a_full_test 串联五个测试——这是宏观状态机:一个完整的 Agent 行为约束网络。

三层嵌套,层层咬合。Agent 在任何一层"越界",都会被拦下来。

最妙的是:Agent 完全不知道这三层结构的存在。 它只知道"当前这一步我要做什么"。它不需要知道上面还有 big_step,更不需要知道上面还有 composition。就像一只蚂蚁在树枝上爬——它不需要知道这棵树的形状,它只需要沿着树枝走。但树的结构决定了它能走到哪里、走不到哪里。


硬代码控制,到底是什么

市面上几乎所有的 AI Agent 框架都在做"软约束":

  • system prompt 里写规则——Agent 可能忽略
  • output parser 里做格式校验——可以被上游绕过
  • 中间件里做拦截——可能被 token 溢出冲掉

这些方案的共同假设是:Agent 会配合。

我们的假设恰恰相反:Agent 不会配合。 不是因为 Agent “坏”,而是因为 Agent 本质上是概率性的——它的行为不可预测,它的理解可能偏差,它的上下文窗口有限。

所以"硬代码控制"的意思不是"写死几行 if-else"。它的意思是:

建立一套 Agent 无法绕过的约束系统,让规则的执行不依赖于 Agent 的理解和配合。

这不是技术路线之争,这是对 AI Agent 本质的不同认知。

如果你相信 Agent 能靠 prompt 约束住,那你在做 prompt 工程。如果你相信 Agent 需要被外部状态机约束,那你在做Agent 治理


一副镣铐的价值

写到这,我想起一个比喻。

没有约束的 Agent 像一个没有交通规则的城市。每辆车都可以自由行驶——听起来很美好,但结果一定是混乱和事故。

有了状态机这副"镣铐",每条路都有通行规则,每个路口都有信号灯。看起来限制了自由,实际上释放了效率——因为每辆车都不需要猜测"别的车会怎么做",它只需要遵守信号灯。

对 Agent 来说,道理是一样的。当 Agent 不需要思考"我下一步该做什么"、“我是不是该停了”、“这个操作安全吗”——当这些决定权被交还给状态机——Agent 反而可以更专注地做好它唯一该做的事:执行当前步骤。

自由不是想做什么就做什么,而是知道不该做什么就不做什么。

对 AI Agent 来说,真正的能力不是能执行多少种操作,而是在什么状态下能做什么、不能做什么,被精确地定义和强制执行。

这就是我第三天对"硬代码控制——状态机驱动"的全部理解。它不是一套技术方案,它是一种设计哲学:相信规则胜过相信 prompt,相信状态胜过相信上下文,相信镣铐胜过相信自律。


项目地址:github.com/lingopi/openry
下一篇预告:Phase 3b — 当 Agent 需要"主动探索":KnowQL 查询语言与子步骤执行模型的双向重构

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐