跳到主要内容

创建日期:2026-09-17 | 最近更新:2026-09-17 源码事实核对自本地仓库 Narcooo/inkos v1.8.0-4-g6e9979b4(工作区在分支 annotate/core-walkthroughagent-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.1dependencies 只有一项 @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 在历史提交里存在过(4ff1fc013e2a07cb),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.mdTreat 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.mdpi-agent-core 提供 Agent 类——它自己实现了「循环」(在内存里维护对话历史,自动驱动模型与工具的来回)。InkOS 不是重写它,而是在它外面/上面包一层。

注意最后那句的分工口诀:pi-core 管「循环怎么跑」,pi-ai 管「跟哪个模型怎么说话」。 记住它,下面所有对照都落在这条线上。

顺带补一个时间线事实(读 git 历史所得):pi 是 2026-04 引入的(8dd296c2,随后 bb51987f 把版本钉到 0.67.1);2026-08 中旬有一串 refactor: … pi harness 提交c56586ec35bb2efdd1d6d8ece7c04465)把生产流程也收敛到 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.tschapter-review-cycle.ts
checkpointer + thread_idstory/state/*.json 快照 + 章节版本存档 + transcriptstate/manager.tsstate/chapter-workspace.ts
interrupt + Command(resume=)propose_action落盘提议卡 → 客户端带 actionPayload 再派发agent/agent-tools.tsagent/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 mergestartChapter 取最小、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;

出口有三条,且没有一条是「步数用完」

  1. 解析失败即熔断——审计输出解析不出来就跳过自动修稿,理由写得很直白:「避免误改正文」;
  2. 达标退出——passed && score >= 85 && lengthInRange
  3. 无净提升退出——nextAssessment.score >= currentAudit.score + 3 才继续,否则 break。

最后还有一条兜底:回退到最高分版本回退到最高分版本(${bestSnapshot.score} 分 vs 当前 ${currentAudit.score} 分))。

一句话:这是「质量门」和「保险丝」的区别。把循环交给图,你得到的是「跑够 N 步就报错」;自己写循环,你才能表达「没变好就别改了,而且退回到最好的那版」。

4.3 中断:把「等待」变成一份可落盘的数据

这是最有意思的一条。本站 LangGraph 实战篇 里说过「interrupt 就是『重动作确认闸门』的框架实现」——InkOS 确实有这个闸门,但实现路径完全不同。

关键事实propose_actionexecute没有任何一行 await 用户输入(读代码所得,agent/agent-tools.ts)。它同步构造并返回一份数据

return textResult(
[title, summary, "", `Instruction: ${params.instruction}`].join("\n"),
{ kind: "proposed_action", action: params.action, actionPayload,},
);

真正的「挂起」发生在本轮收尾,靠的是注入 pi 的三个钩子——propose_actionisTerminalProductionToolName 名单里(共 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 留下的缝隙」——convertToLlmstreamFn 这两个拦截点,让它能在框架的循环里插一句「你现在给我闭嘴,等人确认」。

在「框架持有循环」的架构里,这件事是最难做的:你没法在框架的循环内部插一个「让模型此刻停止」的决定,只能绕着框架走。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-mono2025-11-21 创建0.73.1(2026-05-07)
@earendil-works/pi-*earendil-works/pi0.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-telemetrytypeboxdiff 等,本站 pi-agent 篇有清单)——这也是「钉版本」在这类快速演进的 harness 上是常见操作的原因。

7. 反过来问:什么时候该用 LangGraph?

这一节必须写,否则就是拉踩。LangGraph 解决的是另一类问题,而且在那些问题上它明显更省事:

你的需求该用什么为什么
图的拓扑本身就是产品(可视化编排、用户拖拽连线、流程编辑器)LangGraph拓扑是数据、要能画能改,图的抽象正是为此而生。InkOS 的拓扑不是产品——它随形态(长篇/短篇/剧本/影游/翻译)与题材变化,是运行期从作品配置里长出来的,画不出来也不需要画
多租户 / 大量并发 thread 的状态隔离LangGraphthread_id + checkpointer 是现成的;自己写要做对不少并发细节
要可观测面板、要快速接一堆现成集成LangGraph(+ LangSmith)生态是它的真实优势
要「状态必须被校验」「完成以落盘为准」「中断要跨进程跨宿主」pi + 自己写本文 4.1–4.4 四条

InkOS 为此付出的代价也要说清楚:没有现成的可视化编排、没有现成的可观测面板、并发与恢复的正确性全部自己保证——它用「顺序 await + 显式取消点」换掉了图带来的调度复杂性,这个交易只有在「拓扑本来就不需要可视化」时才划算。

8. 可迁移的四条判断

把上面的分析收成四条可以拿去问自己项目的问题:

  1. 你的状态需要「被拒绝」吗? 需要「坏数据进不来」,就别只用 reducer 的合并语义——把校验放在写入的出口,并让失败当场抛
  2. 你的循环出口是「步数」还是「质量」? 是质量(打分、阈值、无净提升熔断、回退最优版本),就必须自己写判定——保险丝替代不了质量门。
  3. 你的「人工确认」需要跨进程、跨多个 UI 吗? 需要就把提议落盘成数据,别在框架里阻塞等待。代价是恢复语义变成「带参数重放一次确定性操作」。
  4. 你要的是「框架管循环」还是「你管循环、框架只管连接」? 前者省时间,后者才拿得到「让模型此刻闭嘴」这类循环内部的决定权——这正是 InkOS 选 pi 的全部理由。

9. 一句话收尾

InkOS 没有「选择 pi 而不是 LangGraph」,它是先确定了「状态必须可校验、完成必须落盘、确认必须可恢复」,然后发现这些约束没有一条能交给框架——于是它只借走了 pi 的循环,把其余全部写成了自己的代码。

反过来讲也一样:如果你要的东西 LangGraph 都给得到,那就该用它——自己写这四套机制一点都不便宜。

关联

自测

  1. 为什么说 InkOS 与 LangGraph 的关系不是「迁移」而是「从未使用」?用哪条命令可以验证?
  2. InkOS 的 reducer 和 LangGraph 的 reducer,职责差在哪一句上?(提示:「合并」vs「拒收」)
  3. 审阅循环的三条出口分别是什么?为什么说 recursion_limit 替代不了它们?
  4. propose_action 的工具实现里为什么没有 await 用户输入?那「挂起」发生在哪里?
  5. 「让模型此刻闭嘴」靠的是哪两个注入钩子?为什么这件事在「框架持有循环」的架构里很难做?
  6. 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.tsstate/state-reducer.tsstate/state-validator.tspipeline/runner.tspipeline/chapter-review-cycle.tsproduction/harness.tsagent/agent-session.tsagent/agent-tools.tsagent/pi-stream.tsutils/atomic-file-set.ts
  • 依赖事实:packages/core/package.jsonpnpm-lock.yamlpackages/core/node_modules/@mariozechner/pi-agent-core/package.json
  • 版本与 scope 迁移:本机 npm view @mariozechner/pi-agent-core timenpm view @earendil-works/pi-agent-core time 实测
  • 许可:AGPL-3.0