创建日期:2026-09-17 | 最近更新:2026-09-17 源码事实核对自本地仓库
Narcooo/inkosv1.8.0-4-g6e9979b4(工作区在分支annotate/core-walkthrough,agent-session.ts带未提交的教学注释改动,该文件行号可能与 master 漂移)。依赖/版本事实来自package.json、lockfile 与本机npm view实测。 ⚠️ 重要声明:仓库里不存在任何选型 ADR 或「为什么不用 LangGraph」的自述(无docs/adr/,全仓langchain|langgraph零命中)。本文凡「为什么这么选」的部分,都是从代码结构 + 事后形成的 harness 契约表述反推的,不是作者原话——正文逐处标了「读代码所得 / 文档自述 / 本文推断」。
InkOS 选型拆解:为什么是 pi-agent,不是 LangGraph
第 2 篇把 InkOS 的骨架拆完了。这篇回答一个被问得最多的问题:这套东西为什么不建在 LangGraph 上?
结论不是「pi 比 LangGraph 好」。真实的结论更像一句错配诊断:InkOS 需要的四件能力——可校验的状态、以质量而非步数收敛的循环、跨宿主可恢复的中断、产物齐全的操作真值——恰好都落在 LangGraph 的抽象边界之外,而 InkOS 每一条都自己写了。本文的工作就是把这个「错配」逐条对上号。
1. 先把事实摆出来:一份可 grep 的零依赖
这事最好用命令说话,因为它可复现(读代码所得,命令原样执行):
$ grep -c "langchain\|langgraph" pnpm-lock.yaml
0
$ grep -rniE "langgraph|langchain" --exclude-dir=node_modules --exclude-dir=.git .
(无输出,0 命中)
整个仓库,从 lockfile 到源码到文档,langchain / langgraph 字样为零。 git log -S'langchain' -- '**/package.json' 在全部历史里同样零命中——也就是说,这不是一段「从 LangGraph 迁走」的迁移史,而是「从来没进过那扇门」。
那它在用什么?packages/core/package.json 的运行时依赖只有这些(读代码所得):
"@mariozechner/pi-agent-core": "0.67.1",
"@mariozechner/pi-ai": "0.67.1",
"@sinclair/typebox": "^0.34.49",
"dotenv": "^16.4.0",
"epub-gen-memory": "^1.0.10",
"jszip": "3.10.1",
"js-yaml": "^4.1.1",
"undici": "6.21.3",
"unpdf": "^1.6.2",
"zod": "^3.25.76"
跟编排/循环有关的,只有 pi 那两个包;其余是 schema(typebox + zod)、IO、导出格式类工具库。更极端的是 pi 那一侧的依赖树——pi-agent-core@0.67.1 的 dependencies 只有一项 @mariozechner/pi-ai(读代码所得,来自 packages/core/node_modules/@mariozechner/pi-agent-core/package.json)。
一个容易被忽略的历史注脚
langgraph 字样确实在 git 历史里出现过一次,值得单独说——它出现在一份已删除的第三方项目研究笔记 .filmgame-systematic-study.md 里(研究对象是 mmlong818/filmgame「猫叔的互动影游创作系统」,不是 InkOS 自己)。原文两句(git 历史可得,该文件已在 9ca56ce8 中归档移除):
- **AI is per-action one-shot calls**, NOT an agent loop and NOT LangGraph despite the dep.
`@langchain/langgraph` dependency is present but the structure path doesn't build a graph.
「装了依赖」和「真的在建图」是两件事——这是作者自己在研究别家项目时写下的判断。
本文推断(非作者原话):这份笔记的姿态很说明问题。作者不是「没听说过 LangGraph」,而是研究过一个装了 LangGraph 的项目,并明确判定它没建图。对 InkOS 这样把「运行时真相」看得极重的系统来说,那种「依赖在、图不在」的状态大概正是要避免的。
2. 边界声明:仓库里没有「选型理由」
必须先把话说清楚,否则后面容易变成代言。我在仓库里主动找过「为什么用 pi 而不是 LangGraph」的自述,结果是:
- 没有
docs/adr/、没有架构决策文档; AGENTS.md/CLAUDE.md在历史提交里存在过(4ff1fc01、3e2a07cb),HEAD 上已被删除;- 全仓
langgraph|langchain零命中。
能引用的,全是**「harness 分工契约」式的表述**——它们回答的是「谁负责什么」,不是「我们对比过谁」。但这些恰恰是理解选型的钥匙(文档自述):
| 出处 | 原文 |
|---|---|
README.md(## 工作原理) | InkOS 以 pi-agent harness 作为统一认知与工具调用内核:Agent 理解用户意图并产生结构化 action,宿主执行确定性工具、确认权限、管理状态并以真实文件和 tool result 判定完成。 |
README.en.md(v1.8.0 特性) | One production harness:… share the pi-agent tool loop and typed action/result boundary. Existing pipelines are deterministic, interruptible capabilities rather than parallel natural-language decision engines. |
skills/SKILL.md | Treat InkOS as a pi-agent-centered production harness, not a bag of prompt shortcuts or parallel pipelines. The model interprets requests and emits typed actions; the host owns confirmation, deterministic tools, state, atomic persistence, and artifact truth. |
CHANGELOG.md | 统一 Pi Agent Harness 与专业创作内核:… Pipeline 保留为确定性生产能力,不再与 Agent 形成平行的自然语言决策系统。 |
docs/core-source-reading-guide.md | pi-agent-core 提供 Agent 类——它自己实现了「循环」(在内存里维护对话历史,自动驱动模型与工具的来回)。InkOS 不是重写它,而是在它外面/上面包一层。 |
注意最后那句的分工口诀:pi-core 管「循环怎么跑」,pi-ai 管「跟哪个模型怎么说话」。 记住它,下面所有对照都落在这条线上。
顺带补一个时间线事实(读 git 历史所得):pi 是 2026-04 引入的(
8dd296c2,随后bb51987f把版本钉到 0.67.1);2026-08 中旬有一串refactor: … pi harness提交(c56586ec→35bb2efd→d1d6d8ec→e7c04465)把生产流程也收敛到 pi harness 上。harness 是后加的一层「统一」,不是起点。
3. 四件套对照:LangGraph 的概念,InkOS 拿什么替的
LangGraph 的全部概念就四样:State(Annotation + reducer)、Node/Edge(StateGraph + 条件边)、Checkpointer(thread_id 记忆)、interrupt(人工介入)。一张表对上 InkOS 的落点:
| LangGraph 概念 | InkOS 的等价物 | 代码位置 |
|---|---|---|
Annotation.Root 定义 state 形状 | zod schema 定义领域真相(8 个 schema) | models/runtime-state.ts |
reducer(覆盖 / 追加) | applyRuntimeStateDelta:hooks 按 id merge、facts 按 predicate 替换、summaries 追加,外加单调性守卫 | state/state-reducer.ts |
StateGraph + 节点 + 条件边 | PipelineRunner 的顺序 await + throwIfOperationAborted();审阅循环自带阈值与回退 | pipeline/runner.ts、chapter-review-cycle.ts |
checkpointer + thread_id | story/state/*.json 快照 + 章节版本存档 + transcript | state/manager.ts、state/chapter-workspace.ts |
interrupt + Command(resume=) | propose_action → 落盘提议卡 → 客户端带 actionPayload 再派发 | agent/agent-tools.ts、agent/agent-session.ts |
「有没有图」这件事可以直接 grep 验证(读代码所得):packages/core/src 里所有 node/edge/graph/拓扑 的命中,全部属于「互动影游的剧情分支图」这一业务内容(interactive-film/graph-store.ts 等),跟编排无关。runner.ts 里没有任何 Node/Edge/Graph/Step/Workflow 类。(packages/studio 依赖 @xyflow/react,但那是前端画流程图的可视化库,不在 core 的编排路径上。)
4. 逐条看:为什么这些替换不是「更土」
对照表只能说明「换了」,不能说明「换得值」。下面五条是 InkOS 把东西拿回自己手里的具体收益,每条都有代码落点。
4.1 状态:reducer 管「怎么合并」,不管「合不合法」
LangGraph 的 Annotation 只表达合并语义——(a,b)=>b 是覆盖、(a,b)=>a.concat(b) 是追加(本站 LangGraph 入门 里实测过)。它没有位置表达「这份新状态是否合法」:校验得你自己塞进节点里,而且忘了塞不会报错。
InkOS 把校验放在了 reducer 的出口——合并完立刻自校验,不合法直接抛(读代码所得,state/state-reducer.ts):
const issues = validateRuntimeState(next);
if (issues.length > 0) {
throw new Error(issues.map((issue) => `${issue.code}: ${issue.message}`).join("; "));
}
而且它的 reducer 比框架默认语义细得多:hooks 是按 id merge(startChapter 取最小、lastAdvancedChapter 取最大、文本取「更丰富」的一侧、状态按 resolved > progressing > 原值 优先级合并),facts 是按 predicate 替换(先删同 predicate 旧 fact 再 push,保留 validFromChapter / validUntilChapter 时序三元组),summaries 才是追加。再加一道单调性守卫:
if (allowReapply ? delta.chapter < snapshot.manifest.lastAppliedChapter
: delta.chapter <= snapshot.manifest.lastAppliedChapter) {
throw new Error(`delta chapter ${delta.chapter} goes backwards`);
}
validator 还做跨文件不变量——这是单文件 schema 表达不了的一类错误(读代码所得,state/state-validator.ts):重复 hookId、重复 summary chapter,以及
if (manifest && currentState && currentState.chapter > manifest.lastAppliedChapter) {
issues.push({ code: "current_state_ahead_of_manifest", … });
}
一句话:LangGraph 让你声明字段怎么合并;InkOS 要的是拒收坏数据。前者的失败模式是「状态悄悄变脏」,后者的失败模式是「当场抛错」——写长篇时,后者才是你要的。
4.2 循环:出口该是「质量」,不是「步数」
LangGraph 用条件边造循环,靠 recursion_limit(默认 25 步)防死循环——保险丝而已,它只保证「会停」,不保证「越跑越好」(本站 LangGraph 篇实测过这个上限)。
InkOS 的审阅循环(draft → audit → revise)写死了四个常量(读代码所得,pipeline/chapter-review-cycle.ts):
const DEFAULT_MAX_REVIEW_ITERATIONS = 1;
const PASS_SCORE_THRESHOLD = 85;
const NET_IMPROVEMENT_EPSILON = 3;
出口有三条,且没有一条是「步数用完」:
- 解析失败即熔断——审计输出解析不出来就跳过自动修稿,理由写得很直白:「避免误改正文」;
- 达标退出——
passed && score >= 85 && lengthInRange; - 无净提升退出——
nextAssessment.score >= currentAudit.score + 3才继续,否则 break。
最后还有一条兜底:回退到最高分版本(回退到最高分版本(${bestSnapshot.score} 分 vs 当前 ${currentAudit.score} 分))。
一句话:这是「质量门」和「保险丝」的区别。把循环交给图,你得到的是「跑够 N 步就报错」;自己写循环,你才能表达「没变好就别改了,而且退回到最好的那版」。
4.3 中断:把「等待」变成一份可落盘的数据
这是最有意思的一条。本站 LangGraph 实战篇 里说过「interrupt 就是『重动作确认闸门』的框架实现」——InkOS 确实有这个闸门,但实现路径完全不同。
关键事实:propose_action 的 execute 里没有任何一行 await 用户输入(读代码所得,agent/agent-tools.ts)。它同步构造并返回一份数据:
return textResult(
[title, summary, "", `Instruction: ${params.instruction}`].join("\n"),
{ kind: "proposed_action", action: params.action, actionPayload, … },
);
真正的「挂起」发生在本轮收尾,靠的是注入 pi 的三个钩子——propose_action 在 isTerminalProductionToolName 名单里(共 21 个终态工具名),于是下一轮 convertToLlm 侦测到「刚调完终态工具、后面没有 assistant 文字」,streamFn 随即返回一个本地假流让模型干净停下:
// convertToLlm:侦测"是否刚调完一个终态生产工具、正等宿主接管"
// streamFn:若处于上述"待接管"态,不再让模型续写,用一个本地假流干净停下
这么设计换来三个框架内挂起拿不到的好处(读代码所得):
- 提议就是普通消息,天然随 transcript 落盘 → Studio 侧
deriveResolvedProposals(messages)能从已持久化的消息反推「哪些提议已被确认」,跨进程重启不丢; - 三个宿主各画各的 UI:Studio 渲染确认卡、TUI 提示
Type /confirm to continue, or /cancel to cancel——同一份数据,不是同一个组件; - 确认卡是一次性的(源码注释:
A proposed action is one-shot: once confirmed or rejected the card locks so the production action can't be re-fired.)。
代价也要说清楚:恢复不是「从图的断点续跑到下一节点」,而是客户端带上已确认的 actionPayload 重新派发一次确定性执行(executeConfirmedProductionAction)。它等价于「可恢复的中断」,但语义是「重放一次带参数的确定性操作」,不是「续跑一张图」。
4.4 真相:checkpointer 存「线程状态」,InkOS 存「产物齐全的运行契约」
LangGraph 的 checkpointer 按 thread_id 存图状态:对通用 Agent 足够,对 InkOS 不够——因为 InkOS 要的不是「对话到哪了」,而是「这次运行到底产出了什么、齐不齐、能不能续跑」。
这一个需求被写成了 production/harness.ts,而且作者在 HEAD 提交(6e9979b4)里给它加了中文教学注释,等于把设计意图直接说出来了(文档自述):
模型(pi-agent)只负责"提议"与产出内容字节;宿主负责把这次运行浓缩成一个
ProductionRunSnapshot,并把"快照 + 它的产物文件"当作一整套、原子地落到磁盘。 产物先落盘、快照最后落盘 → 一个 completed 的快照绝不会指向半套文件。 本文件刻意不 import 任何模型/LLM 代码——它是纯宿主侧的数据契约,可以脱离模型单独单测。这体现了 harness 骨架与模型解耦的设计。
它的 ProductionObservation 把「审稿意见」从散文变成了机器可查的偏差记录:
readonly metric: string; // 量什么
readonly expected: unknown; // 期望
readonly actual: unknown; // 实际
readonly evidence: …; // 证据
readonly severity: "info" | "warning" | "blocking"; // 挡不挡路
readonly repairable: boolean; // 能否自动修
外加 resumeCursor 让中断的运行能从中间续跑、needs-review 状态把「算不算完成」留给宿主/人工而不是模型自封。
一句话:这就是 4.1–4.3 的共同根源。LangGraph 的 state 是「流程的 state」,InkOS 的 state 是「作品的真相 + 这次运行的操作真值」。 前者通用,后者必须自己定义——而这正是一个「跑二十万字不出错」的系统真正卖的东西。
4.5 依赖与嵌入:两个包的依赖树,是三个宿主的前提
InkOS 是 CLI + TUI + Studio(Web)三个宿主共用一个 core,还要被外部 Agent 调用(inkos agent / interact)。在这个前提下,「依赖树薄」不是审美偏好,是能不能嵌进别人的运行时的硬需求:
- core 的编排依赖:
pi-agent-core+pi-ai; pi-agent-core的依赖:只有pi-ai一项——树的深度是 2。
再叠上 4.4 里那句「刻意不 import 任何模型代码,可脱离模型单独单测」,结论就是:pi 在这里承担的边界非常窄,窄到可以整体替换而不动业务。 反过来,LangChain 那棵树里厂商 SDK、core、langgraph 一整套都得进来。
5. 别神化 pi:InkOS 只用了它 4 个 API
这一点很多「选型文」会含糊过去,但源码里很清楚(读代码所得)。Agent 类被实际使用的 API 只有:
new Agent({...}) 构造函数
agent.subscribe(...) 订阅事件
agent.prompt(...) 跑一轮
agent.abort() 取消
(外加读写 agent.state.messages)
steer / followUp 一次都没调——尽管 pi-agent-core 明确提供了(steering/followUp 队列、steeringMode 等)。InkOS 的中途干预走的是 abort() + 同会话串行队列(runInAgentSessionQueue,注释写明「同一 project+session 的轮次排队串行」)。
真正让「模型提议、宿主执行」落地的,是注入进去的三个钩子(读代码所得,agent/agent-session.ts):
| 钩子 | InkOS 拿它做什么 |
|---|---|
convertToLlm | 记录「是否刚调完终态工具、正等宿主接管」 |
streamFn | 待接管态下用本地假流停住模型,不让它无中生有继续写 |
getApiKey | 统一取 key 的位置 |
外部传输还被再包了一层:guardedPiStream 是唯一的 Pi 传输边界(注释原文:The single Pi transport boundary used by both conversational and worker agents.),在它里面加上下文窗口守卫、轨迹头、取消与流超时。
这一节才是选型的真正理由。 InkOS 不是在用「pi 提供的能力」,而是在用「pi 留下的缝隙」——
convertToLlm和streamFn这两个拦截点,让它能在框架的循环里插一句「你现在给我闭嘴,等人确认」。在「框架持有循环」的架构里,这件事是最难做的:你没法在框架的循环内部插一个「让模型此刻停止」的决定,只能绕着框架走。InkOS 选 pi,本质是选「循环的所有权」。
6. 顺带一个版本坑:InkOS 钉的是旧 scope
既然本站 pi-agent 系列 写的是 @earendil-works/pi-agent-core@0.85.1,而 InkOS 钉的是 @mariozechner/pi-agent-core@0.67.1,这两个名字会让你以为看错了。它们是同一个项目(本机 npm view 实测):
| scope | 仓库 | 首版 | 末版 |
|---|---|---|---|
@mariozechner/pi-* | badlogic/pi-mono | 2025-11-21 创建 | 0.73.1(2026-05-07) |
@earendil-works/pi-* | earendil-works/pi | 0.74.0(2026-05-07) | 0.85.1(2026-09-05) |
2026-05-07 当天,项目换了仓库和 scope,版本号从 0.73.1 无缝接到 0.74.0。 InkOS 钉的 0.67.1 发布于 2026-04-13,属于改名前的旧 scope。
实践含义:如果你读 InkOS 代码去
npm view @mariozechner/pi-agent-core,会看到它「停在 0.73.1 不动了」——不是项目死了,是改名了。而 0.74.0 之后 pi-agent-core 的依赖也从「只有 pi-ai」长到了好几个(pi-telemetry、typebox、diff等,本站 pi-agent 篇有清单)——这也是「钉版本」在这类快速演进的 harness 上是常见操作的原因。
7. 反过来问:什么时候该用 LangGraph?
这一节必须写,否则就是拉踩。LangGraph 解决的是另一类问题,而且在那些问题上它明显更省事:
| 你的需求 | 该用什么 | 为什么 |
|---|---|---|
| 图的拓扑本身就是产品(可视化编排、用户拖拽连线、流程编辑器) | LangGraph | 拓扑是数据、要能画能改,图的抽象正是为此而生。InkOS 的拓扑不是产品——它随形态(长篇/短篇/剧本/影游/翻译)与题材变化,是运行期从作品配置里长出来的,画不出来也不需要画 |
| 多租户 / 大量并发 thread 的状态隔离 | LangGraph | thread_id + checkpointer 是现成的;自己写要做对不少并发细节 |
| 要可观测面板、要快速接一堆现成集成 | LangGraph(+ LangSmith) | 生态是它的真实优势 |
| 要「状态必须被校验」「完成以落盘为准」「中断要跨进程跨宿主」 | pi + 自己写 | 本文 4.1–4.4 四条 |
InkOS 为此付出的代价也要说清楚:没有现成的可视化编排、没有现成的可观测面板、并发与恢复的正确性全部自己保证——它用「顺序 await + 显式取消点」换掉了图带来的调度复杂性,这个交易只有在「拓扑本来就不需要可视化」时才划算。
8. 可迁移的四条判断
把上面的分析收成四条可以拿去问自己项目的问题:
- 你的状态需要「被拒绝」吗? 需要「坏数据进不来」,就别只用 reducer 的合并语义——把校验放在写入的出口,并让失败当场抛。
- 你的循环出口是「步数」还是「质量」? 是质量(打分、阈值、无净提升熔断、回退最优版本),就必须自己写判定——保险丝替代不了质量门。
- 你的「人工确认」需要跨进程、跨多个 UI 吗? 需要就把提议落盘成数据,别在框架里阻塞等待。代价是恢复语义变成「带参数重放一次确定性操作」。
- 你要的是「框架管循环」还是「你管循环、框架只管连接」? 前者省时间,后者才拿得到「让模型此刻闭嘴」这类循环内部的决定权——这正是 InkOS 选 pi 的全部理由。
9. 一句话收尾
InkOS 没有「选择 pi 而不是 LangGraph」,它是先确定了「状态必须可校验、完成必须落盘、确认必须可恢复」,然后发现这些约束没有一条能交给框架——于是它只借走了 pi 的循环,把其余全部写成了自己的代码。
反过来讲也一样:如果你要的东西 LangGraph 都给得到,那就该用它——自己写这四套机制一点都不便宜。
关联
- 前置:InkOS 入门、深度体验、架构拆解(本文是第 2 篇的选型延伸)
- 对照框架:LangChain + LangGraph 系列(State/Node/Edge、reducer、checkpointer、interrupt 的真实用法与实测)
- 底层运行时:pi-agent 系列(pi-ai 接入层 / pi-agent-core 的 Agent 与事件流)
- 框架横评(含同任务实测):LangChain 还是 pi-agent?(本文是它的「落到具体项目」版)
- 不依赖任何框架的最小实现:frontend-agent
自测
- 为什么说 InkOS 与 LangGraph 的关系不是「迁移」而是「从未使用」?用哪条命令可以验证?
- InkOS 的 reducer 和 LangGraph 的 reducer,职责差在哪一句上?(提示:「合并」vs「拒收」)
- 审阅循环的三条出口分别是什么?为什么说
recursion_limit替代不了它们? propose_action的工具实现里为什么没有await用户输入?那「挂起」发生在哪里?- 「让模型此刻闭嘴」靠的是哪两个注入钩子?为什么这件事在「框架持有循环」的架构里很难做?
- InkOS 钉的 pi 版本属于哪个 scope?为什么会看到「0.73.1 之后就不更新了」?
参考
- 仓库:
Narcooo/inkos(本地克隆v1.8.0-4-g6e9979b4,2026-09-07;工作区分支annotate/core-walkthrough) - 关键源码:
packages/core/src/models/runtime-state.ts、state/state-reducer.ts、state/state-validator.ts、pipeline/runner.ts、pipeline/chapter-review-cycle.ts、production/harness.ts、agent/agent-session.ts、agent/agent-tools.ts、agent/pi-stream.ts、utils/atomic-file-set.ts - 依赖事实:
packages/core/package.json、pnpm-lock.yaml、packages/core/node_modules/@mariozechner/pi-agent-core/package.json - 版本与 scope 迁移:本机
npm view @mariozechner/pi-agent-core time、npm view @earendil-works/pi-agent-core time实测 - 许可:AGPL-3.0