OpenProgram Docs

总览:跟着一个场景走一遍#

不堆术语,先看一件事从头到尾怎么发生。看完这一篇,整层在干嘛你就懂了。

场景:模型想跑 rm -rf build,框架把它拦下来#

agent 正在帮用户清理项目。某一刻,模型决定执行一条 shell 命令 rm -rf build。 我们希望框架在这条命令真正跑起来之前,看一眼,发现它危险,拦下来问用户。

整个过程是这样的:

场景:拦下 rm -rf build 的五步

就这么简单。现在把这五步里出现的几个概念,逐个讲清楚。

概念 1:事件(Event)#

事件 = "刚刚发生了一件事"的一条记录。 上面第②步那个带花括号的东西就是一条事件。

agent 干活的过程里,到处都在产生事件:

发生的事 事件
用户发了消息 user.prompt_submitted
模型开始/说完回复 model.response_started / model.response_completed
工具即将执行 tool.before ← 第②步就是这条
工具执行完了 tool.after
某个文件被改了 file.changed

每条事件就是一个小数据包,记着"什么类型、什么时候、谁干的、相关的内容是什么"。 事件长什么样、字段有哪些,下一篇 events-and-state.md 细讲。

关键心智模型:把事件想成一条不断变长的流水账,只往后记、不涂改。系统里发生的一切 都在这条账上留一笔。你的主动规则不直接盯着 agent 内部,而是盯着这条流水账。

概念 2:规则(Policy)#

规则 = "盯着某类事件,事件来了就判断要不要出手"的一段逻辑。 第④步那条 DangerousCommandGuard 就是一条规则。

一条规则是一个普通 Python 类,说清三件事:

class DangerousCommandGuard:
    # 1. 我盯着哪类事件?
    on = {"tool.before"}

    # 2. 事件来了,我怎么判断?
    def evaluate(self, event):
        命令 = event.payload["command"]
        if "rm -rf" in 命令:
            return 拦下来("这条命令会删文件,先确认")  # 出手
        return None                                    # 不出手,放它过去

on 说"我只关心工具即将执行这类事件",框架就只在这类事件发生时唤起它。 evaluate 拿到事件,看一眼,要么返回一个"动作"(出手),要么返回 None(不管)。

整个框架就是一堆这样的规则。加一条新主动能力 = 写一个新 Policy 类。 你以后想让框架 "发现没写测试就提醒""发现模型卡住了就介入",都是再写一个这样的类,不用动框架内核。

概念 3:两种规则——"挡路的"和"旁观的"#

不是所有出手都一样。有两种本质不同的出手时机,框架把规则分成两类:

挡路的(gate) 旁观的(observer)
它在哪出手 在事情发生之前,拦住 在事情发生之后,看着
例子 "这命令危险,先别执行" "你改了核心代码但没补测试,提醒一下"
必须很快吗 必须。它挡在路中间,agent 在等它放行,它慢一毫秒 agent 就卡一毫秒 不必。它在旁边慢慢想,想完了再说,不耽误 agent
出手方式 拦下 / 放行 / 要用户确认 给个提醒 / 后台默默做点准备

为什么非要分两类、不能合成一套?因为它们的时间要求是反的:挡路的必须快(不能让 agent 等),旁观的可以慢(慢点没关系,但不能反过来拖慢 agent)。把这两种揉一起,要么旁观的拖慢了 agent,要么挡路的为了快牺牲了能力。分开各管各的,互不拖累。

DangerousCommandGuard 是挡路的。"没补测试就提醒"是旁观的。规则怎么写、两类各有什么讲究, execution-model.md 细讲。

概念 4:出手的方式(Action)#

规则的 evaluate 决定出手时,返回的不是随便什么东西,而是几种固定的"动作"之一:

动作 干什么 谁用
拦下/放行/问用户 挡住一个即将执行的工具 挡路的规则
提醒用户 弹个非打扰的提示 旁观的规则
给模型注入一句话 在模型下次思考前,悄悄塞一句提示(不打扰用户) 旁观的规则
后台默默做准备 起一个只读的后台小任务,先把功课做了,有结论再决定要不要提醒 旁观的规则

规则只负责"决定出手 + 选哪种动作",具体怎么落地(怎么弹窗、怎么拦、怎么注入)由框架做。 规则不碰这些脏活,所以规则能写得很短、很专注。

概念 5:状态(State)——这层为什么值得做#

到这你可能想:这不就是"在工具执行前插一段检查代码"吗?我直接挂个钩子不就行了,要这么一套框架干嘛?

对,如果每条规则都只看眼前这一件事,钩子确实够了,不需要这套框架。 这套框架真正值钱的地方, 在你要做这种规则的时候才显出来:

"模型连续三次调同一个工具都失败了,提醒用户它可能卡住了。"

注意"连续三次"——这条规则要判断,得记住前面发生过什么,不是看眼前一下。

钩子做这个会很别扭:你得自己在某个地方存一个计数器,每次失败手动加一、成功手动清零、还得管 多个会话别串台。每加一条"需要记忆"的规则,就手搓一个这样的容器,三五条之后一团乱。

事件驱动天然解决这个:既然每件事都记成了事件(那条流水账),"当前状况"就是把流水账从头 累一遍的结果。 想知道"这个工具最近失败几次"?数一遍账上这个工具的失败事件就行——不用你 手动维护计数器,它是事件的自然副产品。

这个"把一长串事件累加成当前状况"的动作,有个名字叫 fold(也叫 reduce)。别被词吓到, 就是滚雪球:从空白开始,一条条事件滚过去,雪球(状态)越滚越大,滚完就是"现在的状况"。

fold:事件一条条过,状况一步步长

这个累加出来的"当前状况"就叫 State(状态)。规则的 evaluate 除了看当前这条事件, 还能读这个 State:

def evaluate(self, event, state):
    if state.某工具连续失败次数 >= 3:
        return 提醒("这个工具连续失败了,可能卡住了")

State 怎么从事件累加出来、为什么这么设计,events-and-state.md 细讲——那是整层的地基。

把五个概念串起来#

五个概念怎么串起来

这就是全部。剩下的文档都是把这张图里某一块讲细:

  • events-and-state.md:事件长啥样、State 怎么 fold 出来
  • execution-model.md:规则怎么写、两类规则的讲究
  • policies-mvp.md:三条真规则当样板
  • invariants.md:框架自己要守的底线(别绕成死循环)

这套设计刻意不做的事#

为了让地基清爽,下面这些不在范围内(归档在 _research_archive/,要加也加得上、不返工):

  • 把事件落盘做到防崩溃恢复、防篡改——研究/生产级的可靠性装修。
  • 离线回放(拿历史会话验证新规则误报率)——写论文才需要。
  • 对抗安全(防恶意注入、密钥脱敏)——把对手当善意用户的简化下不需要。
  • 复杂的打扰预算、自动熔断——改用最简单的冷却(同一提醒隔一阵才再来)承担这件事。

这套设计的目标只有一个:一个能跑、能不断加规则、规则能记住过去的事件驱动地基。

Last updated · 2026-08-13