Cairn 与面向目标的状态空间搜索

本文以本地克隆的 oritera/Cairn(main 分支,提交 8e7e0ea,2026-09-07)为对象,逐文件阅读源码,对照仓库内的 docs/specs/*.md 协议文档与作者复盘,并在本机运行测试套件验证。论述重点为两项:其一,该系统如何把渗透测试重述为面向目标的状态空间搜索;其二,仅由决定与执行两类活动构成的极简循环,其运行条件与代价为何。

GitHub - oritera/Cairn: Cairn is a general-purpose AI state-space search engine, validated first on autonomous penetration testing. Sometimes misspelled as Carin. · GitHub

标注约定。 依照本专题的规则,文中区分三类陈述,以免将作者的声明与本文的推断混用:

标记 含义
原作自述 作者在 README.md、docs/specs/*.md 或复盘中明确写出
源码推得 作者未直接说明,但实现可逐项对应,由本文阅读代码得到
本文推断 本文提出、尚无上游依据的判断

1 对象与背景

1.1 开源版与 Cairn_Y 的区分

讨论 Cairn 时须先区分两个对象,否则容易把可读代码与公开成绩混为一谈。开源仓库 Cairn 是作者在腾讯云黑客松期间完成的一次工程实践。作者自述其定位「不是一个功能完备的渗透测试项目,而是当时我对极简主义、黑板架构、Fact-Intent 图的理解与运用的一次工程实践」。此后在同一架构上继续演进的版本称为 Cairn_Y,未开源。二者的差异集中于图结构的命名、目标层级的表达以及工程栈的选择:

维度 开源 Cairn Cairn_Y
图结构 Fact–Intent FGS(Fact–Goal–Step)
过程节点 intent step
目标表达 单一 goal 节点 goal 与可动态增删的 Sub Goal
搜索产物 仅 Fact Fact 与 Finding
两类活动 reason / explore Decide / Execute
Agent Loop Claude Code、Codex、Pi 适配层 基于 Pi 的库,仅使用其 Agent Loop
实现语言 Python Node,后续倾向 Go 或 Rust

公开榜单成绩属于 Cairn_Y:Cybench 满分第一,TSecBench v1 第一(63 题、74 个 flag),XBOW Validation Benchmarks 第一。作者同时说明,这些成绩未针对赛题优化提示词、未内嵌答案、未携带跨轮记忆,仅在 Harness 工程层面迭代;TSecBench 公开全部 LLM 记录并提供作弊举报通道。开源版本自身的成绩是腾讯云黑客松智能渗透挑战赛第二届的 54/54,为当场唯一 AK,总排名第三。

值得记录的还有成本变化。同一评测集全解,半年前约需 7000 元,Cairn_Y 目前不足 50 元,而架构主体并未改变。这一数量级的差异并非来自性能优化,而来自若干机制的取消:不加载 Skill、不做 RAG、不做多 Agent 之间的上下文传递、不做跨题记忆。极简在此处表现为成本结构,而非风格偏好。

1.2 问题的形态

将目标、工具与历史消息一并交给模型并令其连续调用,是当前长任务 Agent 的主流形态。该形态假设路径可以被预先枚举,因而在任务偏离预设时出现三类失效:阶段划分刚性,已进入后置阶段便难以退回;规划与执行共享同一上下文,模型易将计划误判为已完成;终止条件通常由模型自行宣告,缺乏外部结构予以否证。

Cairn 的回应是替换这一假设:路径不可枚举,但状态可枚举。由此,任务由「执行完预定流程」转为「在状态空间中寻找一条通向目标的路径」。这并非修辞上的改写,因为它同时提供了可执行的三项要素:下一步从何而来(frontier)、一步如何计为完成(后继)、何时终止(目标测试)。


2 问题的形式化

2.1 FGS:状态、算子与终止条件

开源版本以 Fact–Intent 图表达状态空间,Cairn_Y 将其正式命名为 FGS 图。作者对三者给出了明确界定:Fact 描述当前已经确认的事实,即当前的世界状态;Goal 描述项目的完成条件,即搜索的终止条件,并允许存在可动态增删的 Sub Goal;Step 描述下一步要做什么,即如何由已有事实产生新的事实,是推动世界状态演化的因果过程(原作自述)。

概念 语义 开源实现位置 Cairn_Y 的变化
Fact 已确认的客观发现,只增不改 server-protocol.md:47-58 语义不变
Intent / Step 由若干 Fact 出发的一次探索,是图的边 server-protocol.md:60-68 更名 Step,强调因果过程
Hint 图外注入的人类判断,不参与因果 server-protocol.md:89-95 保留
Goal 特殊的 Fact,同时为终点节点 server-protocol.md:53-58 增加 Sub Goal
Finding 无 — 新增,搜索过程的结构化产物

Intent 更名为 Step,其动因可从词义分辨:Intent 指行动者的主观意图,Step 指旧事实推出新事实的客观过程。更名后,图中不再记录 Agent 的打算,只记录世界演化的链条,与其「记忆外化」的主张一致。

Finding 的引入回应了一个实际需求。CTF 场景中搜索产物与 Goal 重合,flag 即目标;但渗透测试与代码审计的交付物是过程中发现的漏洞,而非「已完成全部测试」这一终态。作者据此把过程产物单独建模,并允许按任务类型动态定义 Finding 的语义。就形式化而言,这一补充是必要的:只有 Goal 的搜索系统无法表达沿路径获得的中间成果。

2.2 与经典搜索的对应

下图以开源版本的数据结构表示一次搜索过程。尖角框为 Intent(边),圆角框为 Fact(状态)。i003 表示多源超边,即 f001 与 f002 共同支撑一次探索。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
flowchart LR
O((origin<br/>起点)):::origin
G((goal<br/>终点)):::goal
F1[f001]:::fact
F2[f002]:::fact
F3[f003]:::fact
I1>i001]:::intent
I2>i002]:::intent
I3>i003<br/>多源超边]:::intent
I4>i004<br/>complete]:::done

O --> I1
I1 --> F1
O --> I2
I2 --> F2
F1 --> I3
F2 --> I3
I3 --> F3
F3 --> I4
I4 --> G

classDef origin fill:#14b8a6,color:#fff,stroke:none
classDef goal fill:#f43f5e,color:#fff,stroke:none
classDef fact fill:#eef2ff,stroke:#6366f1
classDef intent fill:#fff7ed,stroke:#f97316
classDef done fill:#ecfdf5,stroke:#10b981

该结构可逐项映射到教科书中的搜索要素,且每一项都有明确的代码落点,而非仅作类比:

搜索要素 Cairn 的实现 代码位置
状态 facts 表的一行,内容为自然语言 db.py:32-37
初始状态 特殊 Fact origin projects.py:83-94
算子 intents 表的一行,from 指向待产出事实 db.py:39-50
后继 conclude 写入新 Fact routers/intents.py:118-147
目标测试 complete,其 from 必须指向已存在事实 contracts.py:81-86
frontier to_fact_id IS NULL 的意图集合 loop.py:298-318
扩展顺序 最新优先,即栈式议程 loop.py:315-318
终止 写入一条指向 goal 的边 projects.py:257-300

from 允许数组,因此 Intent 在图上等价于一条超边,多个已知事实可共同支撑一次探索。这一点区别于线性日志:因果关系不必被压缩为单条链,上下文也不会因择一主事实而丢失。

2.3 完成即一条边

多数 Agent 系统以模型输出的一句话作为完成判据。Cairn 将完成也纳入图结构。complete_project 执行三项操作:插入一条 to_fact_id='goal' 的意图、将 from 中的每个事实登记为该意图的源、将项目状态置为 completed(projects.py:257-300)。由此,成功等价于图上出现一条由既有事实指向 goal 的边。

这一处理带来三项性质。其一,完成必须引用已存在的 Fact(services.py:79-87),不能凭空宣告。其二,完成在界面上表现为一条可见的边。其三,完成可被撤销,因为它是数据而非记忆。撤销机制由 reopen 实现:外部判定原完成结论有误时,系统删除该完成边,新增一条纠错 Fact,并以 external_feedback 为描述建立一条指向原源事实的超边,项目回到 active(projects.py:303-357)。错误结论未被删除,而是转为可用于后续规划的负样本。

2.4 界面证据

仓库自带一张运行截图(README/cairn.png),可佐证上述机制在真实运行中的形态:

图中左侧为 Origin、多条带意图描述的边、若干 Fact 与 Goal,节点与边的关系与前述形式化一致。右侧详情面板列出该事实的来源:Intent i004、From f002、Creator、Worker、Concluded 00:21:11,即任一结论均可回溯至产生它的那一步、所依赖的前提与执行者。

更值得注意的是该事实的正文:f002's claim that flag was at /var/www/html/flag.php was incorrect — flag.php is the app source, not the flag。这印证了「事实只增不改、状态变化以追加表达」并非纸面约定:错误的断言未被删除,而是被后续事实修正。正文末尾的 its content was not yet read before conclude phase 则表明,该事实由收尾阶段写入,主阶段未及完成的观察仍得以保留。


3 Decide 与 Execute

3.1 两类活动的定义

系统只有两类活动,且二者是独立的运行过程,而非固定角色的 SubAgent。按作者表述,可将二者理解为同一个运行器被注入了不同的提示词与工具:

Decide Execute
工具面 仅查看与操作 FGS 图的工具 读图工具,及可改变世界状态的工具(read、bash、edit、write)与 submit_fact
触发条件 任务开始,或图上信息增加、变化 存在可执行的 Step
并发 串行,全局同一时刻仅一个 可并行
上下文 每次触发以干净上下文重新运行,不携带记忆 无跨会话记忆
职责 评估 Step 的新增、废弃与优先级,增删 Sub Goal,判断 Goal 是否达成 执行一步探索,产出新 Fact

作者的补充说明了这套划分的实质:「两类活动的本质其实都是注入了不同 Tool 和 Prompt 的 Agent Loop,但都没有独立的跨会话记忆机制,FGS 图就是他们共同的、外化的记忆。」据此,记忆不再是一个独立子系统,没有向量库、摘要器或会话回放;需要记住的内容写入图,需要读取的内容重读图。

3.2 串行与并行的依据

Decide 串行而 Execute 可并行,并非任意约定,其依据在于二者写入的对象不同。

Decide 读取全图并写入议程,即新增、废弃或排序 Step。若两个 Decide 并行,它们将基于同一份旧图各自派生一批 Step,导致重复与优先级冲突,因此该环节必须是单写者。开源实现中对应的是项目级 reason 租约:claim_project_reason 检查 reason_worker 是否已被占用,冲突返回 409(projects.py:191-216)。

Execute 读取全图但只追加一条 Fact,不修改历史。多写者之间的结果天然可合并,这一自由度正是「事实只增不改」换来的。实现中对应的是意图级认领:首次 heartbeat 写入 worker 即取得执行权(loop.py:504),粒度远细于项目级租约。

概括而言,写入议程需要独占,写入事实可以并流。该划分使执行侧可横向扩展,同时避免议程本身出现竞争。

3.3 Decide 的输出契约

reason.md 全文 55 行,将 Decide 的可能输出限定为三种。目标已达成时返回 complete,其中 from 必须来自合法事实列表;目标未达成时应推进时返回 intents 数组;目标未达成但当前不应派生新方向时返回空对象。

1
2
3
{"accepted": true, "data": {"complete": {"from": ["f001"], "description": "..."}}}
{"accepted": true, "data": {"intents": [{"from": ["f001"], "description": "方向 A"}]}}
{"accepted": true, "data": {}}

输入亦仅三项:图快照、当前合法 Fact id 列表、尚未结论的意图列表(reason.md:41-55)。契约由代码强制,validate_reason_payload 实现五条约束(contracts.py:62-101):

约束 行为 作用
complete 与 intents 互斥 同时出现即抛出异常 排除「既宣告完成又继续搜索」的模糊状态
frontier 为空时 intents 不得为空 抛出异常 避免无待办又不派生方向导致的停滞
仅返回空对象且 frontier 非空 允许 已有足够待办时不必新增
intents 超出上限 截断至 max_intents 抑制 frontier 膨胀
from 引用不存在的事实 服务端返回 404 保证引用完整性

表中第二条与第三条共同界定了 frontier 为空与非空两种情形下的合法输出。当 frontier 为空时,模型若声称无可推进方向,即构成契约违规;系统记录 reason created no intents 并记为失败,由下一轮重试(reason.py:263-272)。据此,系统活性由契约的不可满足性保证,而不依赖模型的自觉。

3.4 Execute 的输出契约

explore 的契约更窄,仅返回一个 description 字符串(contracts.py:157-170)。其中不含状态、边、置信度或证据对象。这一设计成立的前提是写图动作不在模型手中:模型只报告本次探索得到的增量,Dispatcher 代其调用 POST /intents/{id}/conclude,由服务端建立 Fact、建立边并将意图置为已结论(routers/intents.py:118-147)。

explore.md 另规定 description 只包含最新发现的增量事实,不重复图快照中已有信息(explore.md:22)。该规则实际承担事实原子化的职责:若每个 Worker 都复述全貌,图将被重复内容占据,Decide 每轮读到的信息熵随之下降。限定为增量,等价于要求每个 Fact 表示一次搜索步的净收益。

3.5 触发条件

Decide 并非定时运行,而由图上状态的变化触发。_reason_trigger 维护一个三元组检查点,仅在三类变化发生时返回触发原因(loop.py:704-718):

1
2
3
4
5
6
if len(project.facts) > checkpoint.fact_count:
changes.append(f"facts:{checkpoint.fact_count}->{len(project.facts)}")
if len(project.hints) > checkpoint.hint_count:
changes.append(f"hints:{checkpoint.hint_count}->{len(project.hints)}")
if checkpoint.open_intent_count > 0 and open_intent_count == 0:
changes.append(f"open_intents:{checkpoint.open_intent_count}->0")

即:事实数量增加、人类注入新的 Hint、既有待办全部结论。设计文档明确指出,意图数量增加不作为触发条件,因为该变化通常仅反映上一次 Decide 自身派生了新方向,并不代表世界状态改变(dispatcher-design.md:583)。检查点的初始化时机亦有处理:若项目启动时已存在未结论意图,Dispatcher 先建立基线检查点,避免运行期间第一次新增事实被忽略(loop.py:858-878)。

3.6 单次执行的时序

一次 Execute 的完整过程涉及认领、续租与两阶段收尾,其复杂度高于单次模型调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sequenceDiagram
autonumber
participant D as Dispatcher
participant S as Cairn Server
participant W as Worker
participant L as 心跳线程
D->>S: 读取项目状态与图快照
D->>S: heartbeat(同时完成认领)
D->>L: 启动租约,周期 3s
D->>W: 执行 CLI,提示词中仅给出快照路径
loop 每 3 秒
L->>S: heartbeat
end
W-->>D: 返回 JSON 或超时
alt 返回合法结果
D->>S: conclude,写入 Fact
else 超时或解析失败
D->>W: 同一 session 再次下发收尾提示词
W-->>D: 仅总结已确认事实
D->>S: conclude,写入 Fact
end

认领即首次 heartbeat,系统未设独立的认领接口。心跳失败的处理分两类:403 与 409 立即判失败,其他错误给予两倍 interval 的宽限;一旦判定失败,直接终止仍在运行的进程(heartbeat.py:88-110),以免失去租约的执行者继续写入。服务端则以心跳超时回收僵尸意图,默认 intent_timeout 为 15 秒(db.py:13-18、services.py:222-237),Dispatcher 启动时校验该值大于自身轮询间隔(loop.py:906-929)。

两阶段收尾是本设计中较为特殊的一处。第一阶段按 300 秒超时正常探索;若超时或输出非法,则以同一 session 下发 explore_conclude.md,限时 90 秒,该提示词要求模型立即停止执行、不再等待未完成命令,仅总结已经确认的事实(explore.py:241-384、explore_conclude.md:19-25)。其作用是将超时由完全损失转为部分收益:一个未得出结论的执行,仍可交付「已确认 A、B、C」这一事实,供后续搜索使用。

3.7 写权限与记忆外置

三条边界共同构成系统的控制面:模型只输出 JSON,不解析自由文本;Dispatcher 是唯一协议写入者,Agent 不认领、不续租、不直接调用 API(dispatcher-design.md:13、:20);状态迁移由服务端的一次 SQL 更新裁决,而非模型的陈述。模型的「我已完成」只是候选信号,使项目转为 completed 的是 complete_project 中的插入语句。

图快照的处理亦值得说明。提示词中不内联图内容,仅给出容器内文件路径,由模型自行读取(common.py:56-70)。此举使提示词体积与图规模解耦,同时将「观察黑板」变为一次工具调用,使 Observe 成为显式步骤。代价是增加一次读取,且模型可能未完整读取。

3.8 实现位置

环节 函数 位置
主循环 DispatcherLoop.run loop.py:80-110
项目调度 _try_dispatch_project loop.py:254-338
触发判定 _reason_trigger loop.py:704-718
派发决定 _dispatch_reason loop.py:366-428
决定主体 run_reason_task tasks/reason.py:33-284
派发执行 _dispatch_explore loop.py:488-546
执行主体 run_explore_task tasks/explore.py:30-238
收尾兜底 _try_conclude_fallback tasks/explore.py:241-384
租约 HeartbeatLease._run runtime/heartbeat.py:88-110
完成落图 complete_project routers/projects.py:257-300
撤销与回溯 reopen_project routers/projects.py:303-357

4 迁移解剖

判断一次迁移的深度,可问:若将原型抽离,该系统会失去什么。就 Cairn 而言,失去的不是一个比喻,而是 frontier、后继与终止三项可执行要素。因此其核心属于问题迁移,而非概念迁移。以下逐项考察它取用的六个原型。

4.1 黑板架构

协议文档将设计渊源明确写为黑板架构,并点名 CMU 的 Hearsay-II(server-protocol.md:15-21)。该原型面对的问题是:语音识别中音素、音节、词与语法多层知识无法由固定流水线组合,局部歧义需由全局约束裁决。其机制由共享黑板、独立知识源与控制器构成,核心是机会式问题求解,控制流不预先固定。

Cairn 的对应关系是:黑板为 SQLite 中的 facts 与 intents,知识源为 Worker(无固定技能,由提示词注入不同工具),控制器为 Dispatcher 的调度函数与 Decide,激活条件为新增事实、新增 Hint 或待办归零。

关键改造在于控制器。经典黑板的控制器是人工编写的启发式优先级规则,可读、可审计、行为可预期;Cairn 将其整体替换为 LLM。收益是系统能够处理从未见过的任务,代价则是收敛性、最优性与可重复性均失去保证,只能依赖目标判据与人工停止兜底。就迁移深度而言,数据结构与控制流属于架构迁移,而控制策略层已不属于原型迁移,应归入对原型的特化。

4.2 图搜索

作者直接使用「状态空间搜索」一词,但未引文献;其学术出处为 Newell 与 Simon 的问题空间假说、A* 算法及教科书中广度优先、深度优先与一致代价搜索(本文推断的出处)。原型面对的问题是:在仅知当前状态与可用算子的条件下,寻找一条到达目标的路径。

Cairn 取用的是完整的问题形式化,而非词汇。状态、初始状态、算子、后继、目标测试、frontier 与扩展顺序均有对应实现(见 §2.2)。这也构成一次可证伪的迁移:既然形式化成立,便应能用搜索领域的指标衡量它,例如到达目标所需扩展的节点数、每解成本、重复扩展比例。

未取用的部分同样明确:无 closed set,即不记录已扩展状态;无 gg 代价与 hh 启发值;无剪枝、双向搜索与迭代加深。这些缺口构成 §6.1 的候选清单。

4.3 工作队列

frontier 在语义上是一个任务队列:Decide 生产未结论的 Step,Execute 领取并消费,to 为空表示尚未完成。相较于成熟实现,它主动省略了三项机制:无优先级,栈式议程即全部策略;无公平性,不防饿死;无死信队列,失败的 Step 直接回到待领取状态(见 §8.2)。这属于简化式迁移,保留队列语义而移除调度策略,与「控制器交给模型」是同一取舍。

4.4 租约

租约机制源自分布式系统,用于在持有者崩溃时避免锁被永久占用:授权附期限,到期自动失效,无需检测崩溃。Cairn 的实现与该机制逐项吻合,尽管文档未引出处(本文推断的出处):

租约要素 实现
授予 首次 heartbeat 写入 worker(loop.py:504)
续租 后台线程每 3 秒续租(heartbeat.py:88-110)
过期回收 超过 intent_timeout 未续租则清空 worker(services.py:222-237)
失效处置 终止仍在运行的进程(heartbeat.py:112-123)
写入校验 conclude 时校验 worker 是否为当前持有者,否则返回 409(services.py:112-121)

最后一项承担了 fencing 的职责,其实现方式是写入前再校验持有者,而非使用单调递增的 token。该方式在单 Dispatcher 下足以拒绝过期持有者的写入,但若允许多实例则会出现经典的 fencing 缺陷,因此设计文档明确限定只支持单实例(dispatcher-design.md:588-589)。这属于以架构约束替代机制完备的做法。

4.5 只追加日志

事实只增不改,状态变化以追加表达,例如「shell 已断开」本身即一条新事实(server-protocol.md:23-31)。这与事件溯源、log-structured 存储及 Git 的只追加有向无环图同源,收益是历史可审计、错误可修正。§2.4 的截图提供了运行时的样本。

但迁移只完成了一半:保留的是结论日志,即图;缺失的是过程日志。对照 ARTEX 的 activity 表,后者记录工具、参数与 Token 消耗,Cairn 无对应结构。因此系统能够回答「确认了什么」,不能回答「如何确认」。

4.6 Stigmergy 与单写者

协调机制采用 Stigmergy,即个体不直接通信,而通过改变共享环境留下痕迹。Agent 之间零通信,只读写同一张图;同时规定 Dispatcher 为唯一协议写入者。这一组合的实际效果是消解了多 Agent 协作中最困难的部分:通信协议与冲突合并不再需要设计,因为写入被串行化,冲突不会发生。就迁移深度而言,把协作问题重述为环境标记问题,带有问题迁移的性质。

4.7 迁移深度汇总

原型 取用内容 位置 深度 收益 代价
黑板架构 共享状态面、无角色知识源、机会式调度 db.py、loop.py:254-338 架构迁移 无预定义流程 控制器换为模型,收敛性无保证
图搜索 状态、算子、后继、目标测试、frontier db.py:32-58、contracts.py 问题迁移 可证伪的收敛判据 缺 closed set、优先级与剪枝
工作队列 frontier 的生产与消费 loop.py:298-318 架构迁移(简化) 执行侧可并行 无优先级、公平性与死信
租约 授予、续租、过期回收、写入校验 heartbeat.py、services.py:222-237 架构迁移(完整) 僵尸任务自动回收 仅支持单实例
只追加日志 事实只增不改 server-protocol.md:23-31 架构迁移(半程) 历史可审计 无过程日志
Stigmergy 与单写者 零通信协调、单一写入者 dispatcher-design.md:13,20 架构迁移 无需通信协议 写吞吐受限于单进程

另需记录其主动未取用的原型:向量检索、工作流引擎、多 Agent 消息总线、PDDL 或 HTN 式规划器、技能库。这五项恰为当前 Agent 工程的主流组件,作者属于有意不用,而非未曾考虑。


5 特化与代价

5.1 为适配 LLM 所作的改造

第一项是控制器的替换。经典黑板的调度由人工规则完成,Cairn 改由模型完成,并以限制输出形状来控制风险:模型只能从三种动作中选择,开放决策被压缩为有限动作集(contracts.py:62-101)。

第二项是算子性质的改变。同一 Step 重跑两次可能得到不同结果。系统未试图消除这一差异,而是接受它并让图承担后果:错误结论由后续事实修正,完成判断有误则由 reopen 撤销。它把算子的不可靠转化为状态的可修正。

第三项是状态表示的改变。经典搜索的状态可哈希、可比对、可索引;Cairn 的状态是一段自然语言,模型可直接读懂,但机器无法判断两个状态是否等价。这一改变直接导致 closed set 无法实现(见 §6.1)。

第四项是失败检测方式的改变。模型不会抛出异常,只会给出不合规的输出。系统将语义层面的失败降级为语法层面的失败,即解析失败或校验失败,从而复用工程上已有的重试与记录路径。

5.2 阻抗失配核对

# 失配项 处理方式 判定
1 确定性 不消除,改为可修正:事实只追加、完成可撤销 部分处理,无法重放
2 寻址方式 无索引,每轮全量读图,以「只写增量」抑制图规模 未处理,是最脆弱环节
3 成本结构 增量触发、干净上下文、12 KB 提示词 处理较好,但与全量读图冲突
4 接口契约 JSON 校验、服务端校验、契约级硬约束 处理最好
5 状态可见性 只外化结论,不外化推理过程 明确接受限制
6 隔离基础 以容器提供边界,而非提示词约束;local 模式放弃隔离 架构层处理,local 模式下失效
7 测量噪声 单次运行,无重复采样与置信区间 未处理

第 7 条需要单独说明。作者公布的榜单成绩是真实的,但「一次达到满分」与「稳定达到满分」是两项不同的性质。缺少方差信息时,无法区分架构有效与单次波动。这是当前 Agent 评测的普遍问题,但在一篇以架构领先为结论的论述中,该项限制应予披露。

5.3 代价汇总

原型提供的能力 系统放弃的部分 换取的能力
可证明的完备性与最优性 无 closed set、优先级与剪枝 可处理无法枚举的开放任务
确定性的算子语义 同一步骤不可重现 可用自然语言描述任意动作
可哈希的状态 无法判断状态等价、无法去重 状态可被模型直接读取
低成本的扩展 每次扩展均为一次模型调用 单步可完成传统算子无法完成的推理
完整的过程记录 无过程日志,无法复盘动作 实现规模小,迁移成本低

综合而言,该系统以放弃搜索算法的严谨性换取自然语言算子的表达能力,并以图结构、租约与契约三项工程约束,将这份自由度限制在仍可收敛的范围内。


6 联想推广

6.1 纵向:尚未取用的机制

Cairn 只取用了状态、算子、后继、目标测试与 frontier 五项,其余机制仍未被使用。下表给出各项在经典搜索中的作用、在 Cairn 中可能的对应形态与落地障碍:

未被取用的机制 经典作用 可能的对应 成本 主要障碍
closed set 避免重复扩展同一状态 事实语义去重 低 自然语言状态难判等价
失败记忆 记录已排除分支 为 Step 增加 blocked 终态 极低 仅需状态字段与一次更新
优先级队列 按价值排序扩展 Decide 为 Step 评分 中 分值由模型给出,噪声较大
剪枝 超出预算的分支直接舍弃 全局步数、Token 与深度上限 低 需定义超支判据
双向搜索 自目标反向扩展 由 Goal 反推所需前置事实 中 反向算子仍需模型生成
记忆化 相同子问题只解一次 子目标级结果复用 中 子问题等价性难判定
事务 一批操作原子生效 一次 Decide 的多条 Step 原子提交 中 需 schema 支持
死信队列 多次失败的任务移出主队列 失败 Step 归档供复盘 低 与失败记忆同源
迭代加深 小预算多次尝试 预算内重启并提高探索温度 中 需可中断的会话
租约优先级继承 防止高优先任务饿死 高价值 Step 优先续租或抢占 低 与栈式议程冲突

其中两项的投入产出比最高。失败记忆仅需增加一个状态字段,即可消除「必然失败的方向被反复领取」这一实际成本,对应经典搜索中最简单的 closed-on-failure。双向搜索的价值在于任务形态:渗透任务的目标通常极为明确,因此可以直接询问「要达成该目标还缺哪条前置事实」,从而自目标反向派生 Step。经典搜索已证明,在分支因子可控时该策略可将指数级搜索降至平方级;而反推前置条件恰是语言模型的强项。本文将这一项视为最值得推进的未发生迁移(本文推断)。

6.2 横向:机制在其他问题上的适用性

先将 Cairn 的机制抽象为其所解决的问题形态:以一张只追加的外部化状态图,协调多个无状态、无通信、可替换的决策者,朝一个可判定的终点收敛。再对候选问题逐一询问四个角色:何者为状态,何者为算子,何者为目标测试,谁写状态。

候选问题 状态 算子 目标测试 写者 缺口
代码审计 代码事实、调用链、数据流结论 一次追踪或验证性 PoC 漏洞可复现 Execute 无
漏洞研究 二进制或协议观察 一次动态或静态实验 崩溃或越权可复现 Execute 无
科研实验 实验结果与文献结论 一次实验 假设获得数据支持 Execute 目标测试难以程序化
数据分析 数据画像与口径结论 一次查询或建模 业务问题获得回答 Execute 目标测试主观
事故响应 证据与时间线片段 一次取证动作 根因获得证实 Execute 证据可信度无对应字段

前两行四项角色齐备,说明该机制具备横向迁移条件,社区中已有基于 Cairn 的二次开发可作佐证。后三行的缺口集中于同一位置:当目标测试无法程序化时,系统的收敛判据失效,需要补充人工裁决或「弱判据加复审」的角色。缺口所在即研究点所在。

6.3 反向:LLM 特化回流经典系统的条件

该方向的成立概率最低,但自检标准明确:须指明 LLM 场景的特殊约束,并判断该约束在经典场景中是否同样成立。

本文归纳的特殊约束有四条(本文推断):算子成本极高,单次扩展可能达数美分至数美元,较经典搜索高若干数量级;结果不可重放,同一输入两次运行结果不同;状态为自然语言,不可哈希、不可精确比对;评价器本身亦为模型,目标测试带有噪声。

据此筛选经典场景,符合「评估昂贵、不要求重放、状态天然为文本」三项条件者有三类:模糊测试的种子调度,可将已确认的覆盖与崩溃写入只追加事实图,并以模型作为变异算子生成器;程序合成的枚举搜索,可用事实图记录已尝试的片段组合与产出,避免重复枚举;材料或分子发现,单次实验昂贵、不要求重放、结果为结构化文本,与 LLM 搜索的成本结构最为接近。

其失效条件亦须写明:需要确定性重放、强实时约束,或状态必须可精确哈希的场景不适用。编译器优化 pass 的搜索与数据库查询计划的枚举即属此类,其算子成本低且确定,引入语言模型反而增加不确定性。

6.4 自检

自检项 结论
角色能否逐一指认 能。§2.2 给出五项要素的代码落点,§6.2 给出横向迁移的四角色表
何种条件下不成立 指标为重复探索率、每解 Token 成本、失败 Step 重领率;基线为当前栈式议程且无失败记忆的 v0.2.1;失效条件为图规模超出上下文可承载范围后,全量读图方案先行失效
是否被失配清单否决 部分否决。「以向量相似度做状态去重」被失配项 2 阻断:语义相似不等价于状态等价,且合并操作具有破坏性,与只追加原则冲突

最后一项是本文自行否决的联想。说明一条路径为何不可行与走通一条路径具有同等价值,因为它标定了该架构的边界:只追加的图不能执行破坏性合并。


7 与 ARTEX 的对照

二者同源于黑板架构,但在多数工程决策上取向相反。

维度 ARTEX Cairn
图的数量 双图:资产图与探索图 单图:事实与意图
状态表示 结构化节点,七类节点、五类边 自然语言文本与一条超边
目标表达 goal 节点与逐条证明 goal 节点,Cairn_Y 增加 Sub Goal
决策者 Planner,读取有损摘要 Decide,读取全图,串行、无状态
执行者 Worker,领取一条意图后停止 Execute,一次一个 Step,可并行
上下文策略 有损摘要与冷节点压缩 全量图快照外置为文件
存储 PostgreSQL,支持条件更新与行锁 SQLite,单机单写者
领取控制 条件更新,等价于 CAS 首次 heartbeat 覆盖写
意图状态 六种以上,含阻塞、耗尽、暂停 两种,以 to 是否为空区分
证据与置信度 confidence 字段、证据引用、流量记录 无对应字段
治理 Guard、人工审批、流量代理 容器隔离,无审批层
过程日志 activity 表 无
交付形态 平台与控制台 引擎与协议

分歧的实质在于对上下文的判断。ARTEX 视上下文为稀缺资源,故构建有损摘要,使模型仅看到热工作集;Cairn 视上下文为可重建对象,故每轮重新读入整图。两种选择各有前提:前者适用于图可持续增长至数万节点的情况,后者要求每个节点被约束为一至两句增量结论。Cairn 用以维持该前提的手段是提示词中的增量要求,属于由模型自觉维持的不变量,一旦失效,全量读取将首先崩溃。

在抗幻觉问题上,二者的着力点不同。ARTEX 以置信度、证据与审批构成证据门,降低结论失真;Cairn 不设此类机制,其依据是渗透场景具备强反馈:shell 连不上、flag 不正确、下一跳不通,错误事实会被环境迅速否证。该依据在强反馈且可即时验证的任务中成立,在反馈稀疏或延迟的场景中不成立。因此 Cairn 的极简具有明确的环境前提,不能整体平移。


8 复现与实测

本机环境为 Windows 与 Python 3.14.7(仓库要求 3.12 以上)。仓库以 uv 管理依赖,本机未安装 uv,故采用虚拟环境直接安装依赖并以 PYTHONPATH 指向源码的等价方式:

1
2
3
4
python -m venv ..\.venv
..\.venv\Scripts\python -m pip install fastapi "uvicorn[standard]" click pyyaml docker requests httpx pytest
$env:PYTHONPATH = 'D:\Cairn\Cairn\cairn\src'
..\.venv\Scripts\python -m pytest cairn\tests -q
轮次 条件 结果
一 直接运行全部用例 84 通过,14 失败
二 排除依赖子进程的用例 68 通过
三 增加 python3 兼容层后运行全部用例 91 通过,7 失败

第一轮失败的 14 项日志均为 code=9009,即 Windows 下找不到 python3 命令;原因在于 mock driver 将命令硬编码为 python3(workers/adapters/mock.py:134)。在虚拟环境的 Scripts 目录为 python.exe 建立 python3.exe 硬链接后,6 项 mock 端到端用例全部通过,其中包含 reason → explore → reason → complete 的完整链路。

剩余 7 项失败集中于 test_local_execution.py,原因属平台语义而非逻辑缺陷:用例使用 sleep、sh 等 POSIX 命令;local_process.py:121 调用 os.killpg 终止进程组,Windows 不支持该接口。

由此可得的结论是:核心协议、调度、契约与服务端接口均通过测试;容器模式下的 mock 全链路通过;local 模式仅支持 POSIX。不能由此得出的结论是:真实模型会严格按契约输出,或真实靶场下的解题率与 Token 成本。上述三项均需接入真实端点方可验证,本文未作此尝试。


9 评价

9.1 设计上的有效部分

形式化清晰是首要优点。状态、算子与终止条件三项均有代码落点,读者无需借助类比即可理解系统行为。其次,两类活动的接口极窄:Decide 仅三种输出,Execute 仅一个字符串,窄接口降低了输出解析失败的概率,也使模型替换的成本很低。第三,收敛判据为数据而非陈述,完成必须引用既有事实并落为一条边,因而可审计、可撤销。第四,上下文可重建,模型可替换、会话可中断、进程可重启,长期状态不受影响。第五,触发条件基于增量,无新态势时不调用模型,这一项直接决定成本结构。

此外,失败处理值得注意:收尾阶段将超时由完全损失转为部分收益,撤销机制将错误结论转为负样本。

9.2 设计代价与风险

以下各项均针对开源版 v0.2.1;Cairn_Y 未开源,除 Finding 与 Sub Goal 两项外无法核实。

预算与状态语义方面存在两项限制。系统仅有单任务 300 秒超时与单次 Intent 数量上限,没有「本项目最多运行若干轮或消耗若干 Token」的约束;若目标始终无法判定达成且每轮均能派生新方向,系统将持续扩张,唯一的制动手段是人工停止。同时,意图仅有已结论与未结论两种状态,释放后仍回到待领取,因此必然失败的方向会被反复领取。排除某方向只能写入一条否定性事实,而此类负结论易被后续规划忽略。Cairn_Y 的 Sub Goal 不解决该问题,因其属于目标层次分解,而非动作结局状态。

事实的可信度与可追溯性方面亦有两项。写入一条事实只需一段描述,模型推断与直接观察在数据结构上完全等价,后续规划均建立其上;作者明确表示不采纳幻觉门控类方案,故此项应视为自觉取舍而非疏漏,但该取舍依赖前文所述的环境前提。系统亦无过程日志,无法回答某条事实由哪条命令产生,复盘依赖模型在描述中自行记录路径。

扩展性与部署方面,全量读图存在隐患:事实数量增至数百后,提示词与注意力成本将显著上升,而抑制图增长的手段仅为提示词中的增量要求。存储为单机单写者,所有并发控制均在进程内完成,无法水平扩展。平台方面,local 模式依赖 POSIX 的进程组语义,在 Windows 上不可用。

正确性与安全方面,完成判定缺少验收器。收敛由 Decide 一次判断决定,虽有引用完整性校验,但「这些事实是否确实证明目标」没有程序化检查,人工撤销是唯一纠错通道。安全边界亦依赖容器而非系统,系统中没有 Guard、审批或流量记录;local 模式明确以用户权限运行且无沙箱。

9.3 适用边界

适用于反馈强、结果可即时验证的任务,例如 CTF、靶场、漏洞研究与具备明确测试的代码修复;亦适用于需要过程可解释的探索型任务,因此整条路径均在图上。不适用于反馈稀疏或判断主观的任务,例如合规审查与写作;不适用于需要严格成本上限或逐动作审计的生产环境;不适用于需要确定性重放的场景;商用闭源亦受 AGPL 与作者附加条款限制。


10 结论

Cairn 的贡献不在于提出新的注意力机制或提示词技巧,而在于给出一种可执行的问题重述:把长任务表示为状态空间中的定向搜索,并将全部状态外化为一张只追加的图。在此基础上,系统只需两类活动——一类维护议程,一类推进状态——即可运行,无需角色划分与预定义流程。

按本专题的判据,这属于问题迁移而非概念迁移:它取用了搜索问题的完整形式化,因而也继承了该形式化的可证伪性。其代价同样来自这一取用:为使自然语言成为算子,系统放弃了状态等价判定、优先级调度与过程记录;为使控制器具备开放语义处理能力,系统放弃了收敛性的保证。这些放弃在强反馈环境中可由环境校验补偿,在弱反馈环境中则不能。

作者对这套架构的自我限定值得引用:「大多数看起来很厉害很有道理的 Agent 架构设计其实并没有用,甚至会起反效果,真正厉害的是模型,不是外层臃肿的 Agent 架构,也不是那一堆冗余的 Skill。」该判断的准确含义并非架构无用,而是架构不应替代模型能力。按此标准,Cairn 只做三件事:外化状态、切分决策与执行、提供收敛判据;其余一概不做。

若要从该系统中取用可迁移的部分,本文认为有四条:将完成表示为一条可审计的边;将决定的输出限制为有限形状;以增量触发替代定时轮询;写入议程时独占、写入事实时并流。


参考

  • 仓库:oritera/Cairn(本地克隆,main @ 8e7e0ea)
  • 方法论总纲:《LLM新技术中的CS原型(零):专题介绍》,本地 D:\PPT\LLM新技术中的CS原型\LLM新技术中的CS原型.md;本文 §4–§6 的框架、三类迁移判据、阻抗失配清单与三向推广均依该文
  • 协议文档:docs/specs/server-protocol.md(864 行;黑板、Stigmergy 与 OODA 的设计渊源见 15-21 行)
  • 调度文档:docs/specs/dispatcher-design.md(1181 行;任务模型与调度策略见 207-600 行)
  • 作者复盘:《AI 自动化渗透架构 Cairn 满分登顶 TsecBench Cybench —— 众人向左,我偏向右》
  • 对照文章:《LLM新技术中的CS原型(四)ARTEX:双图架构解决长任务幻觉问题》
  • 第三方概述(AI 生成,仅作背景参考):Wester · Cairn 拆解
  • 早前尝试的博客链接 https://mp.weixin.qq.com/s/ZzKF_0MOb0cak9izhHqCUQ 抓取时被微信验证码拦截(HTTP 200,内容为验证页),未采信;设计意图的判断改以本地保存的复盘与仓库文档为依据