创建日期:2026-09-14 | 最近更新:2026-09-14 源码级拆解,事实核对自本地仓库
Narcooo/inkosv1.8.0-4-g6e9979b4(2026-09-07):目录结构、文件清单、关键实现(FTS5 检索、37 维审计、状态校验与投影、工具清单)均为读代码所得;标注「未实测」处为纯静态阅读,未运行验证。
InkOS 架构拆解:长篇为什么不出错
第 0 篇说「InkOS = 把模型当有想法的执行者,把创作当有状态、有校验、可回滚的工程」。这篇把这句话落到代码上:agent 分几类、状态怎么存、检索怎么做、审计查什么、一章是怎么原子落盘的。
1. 三层包结构(先看骨架)
packages/
├─ cli/ # 命令入口 + TUI(ink/react)
├─ core/ # ★ 全部核心逻辑:agent / 状态 / 流水线 / 检索 / 提示词 / 技能
└─ studio/ # Web 工作台(Vite + React + Hono)
core/src 下 20 个子目录、459 个 TS 文件,主要几块:
core/src/
├─ agent/ # 会话 harness(Chat Agent 调工具,建在 pi-agent 上)
├─ agents/ # 领域 agent(规划/编排/审计/润色/检测/雷达…)
├─ state/ # 状态:校验、reducer、投影、章节工作区、SQLite 记忆库
├─ pipeline/ # 主流水线:runner、持久化、审阅循环、状态恢复…
├─ production/ # production harness
├─ retrieval/ # 本地检索(SQLite FTS5 + BM25)
├─ models/ # 领域模型:输入治理、字数治理、上下文压缩、题材/文风…
├─ prompts/ # 提示词资产(内置提示包)
├─ skills/ # SKILL.md 体系:内置/外部加载、注册表、生产绑定
├─ interaction/ # 会话与动作:action 信封、会话记录、请求路由、真相权威
└─ notify/ # 通知:telegram / feishu / wechat-work / webhook + dispatcher
2. 两类 Agent:别搞混
InkOS 里有两种完全不同的 agent,理解这点就看懂一半:
agents/(领域 agent) | agent/(会话 harness) | |
|---|---|---|
| 是什么 | 流水线里的固定角色 | 「Chat Agent 调工具」的运行时 |
| 例子 | architect(建基础设定)、planner(本章意图)、composer(选上下文)、continuity(连续性审计)、polisher、detector、ai-tells、chapter-analyzer、consolidator、radar | agent-session、agent-tools、agent-system-prompt、pi-stream、worker-agent、context-transform、skill-tool |
| 谁驱动 | PipelineRunner 按阶段调用 | 用户/外部通过 inkos agent、inkos interact 驱动 |
| 底层 | 直接调 LLM | 建在 pi-agent 上 |
agent/agent-session.ts 的注释直说了这件事(原文要点):「pi-agent 的 Agent 会把完整对话(含工具调用)留在内存里。一个 sessionId …」,并且实现里用队列保证「同一 project+session 的轮次排队串行」。这就是第 0 篇那句「底层运行时:pi(@mariozechner/pi-ai、pi-agent-core)」在代码里的落点——顺带也解释了 inkos agent --session 为什么能跨调用记住上下文。
3. 工具清单:模型「会做什么」全在这张表
agent/agent-tools.ts 里注册的工具(真实名字,节选):
propose_action sub_agent research_web ingest_material
retrieve_material manage_book_reference import_chapters
fanfic_create spinoff_create imitation_create continuation_import
short_fiction_run translation_create script_create
storyboard_create interactive_film_create generate_cover
play_start play_edit play_step
看两个名字就懂设计哲学:
propose_action:重动作不是直接干,而是先提议——它和interaction/action-envelope.ts(动作信封)配合,把「写一章/生成封面/开一本新书」这种重操作打包成结构化请求,交给宿主确认后再执行。这就是第 0 篇「重动作先弹确认卡」在代码里的实现位置。sub_agent:agent 能再开子任务——复杂指令可以拆出去做。
一句话:模型手里只有工具,没有权限。工具列表就是它能触达世界的全部边界。
4. 状态层:长篇不出错的核心
state/ 目录是「可信状态」的实现,四个关键件:
| 文件 | 职责 |
|---|---|
state-validator.ts | 校验:拿到 schema.parse(value) 这样的契约,坏数据直接拒收 |
state-reducer.ts | 把模型输出的 JSON delta 应用成新状态(不是让模型重写整个状态) |
state-projections.ts | 把结构化状态投影成人看的 markdown(current_state.md、pending_hooks.md…) |
chapter-workspace.ts | 章节工作区:saveChapterUserBrief、archiveChapterVersion、listChapterVersions、readChapterVersion——每章有版本归档能力,可回看/回滚 |
外加 memory-db.ts(SQLite 时序记忆库)建了三类表(真实建表语句):
facts -- 事实
chapter_summaries -- 章节摘要
hooks -- 伏笔
这套设计回答了一个关键问题:为什么不让模型直接写 markdown 来「改状态」?因为 markdown 没法校验、没法增量、没法回滚。JSON delta + schema 校验 + 投影才是工程解——这也是第 0 篇里「Markdown 是给人看的投影,JSON 才是权威真相」的来源。
5. 检索:不是「全塞上下文」,而是「按需检索」
retrieval/local-search.ts 里是标准做法(真实代码片段):
CREATE VIRTUAL TABLE IF NOT EXISTS retrieval_documents_fts USING fts5(...);
SELECT ..., bm25(retrieval_documents_fts, 5.0, 1.0) AS rank
FROM retrieval_documents_fts
WHERE retrieval_documents_fts MATCH ?
- FTS5:SQLite 的全文索引(不是暴力扫表);
bm25(...)排序:按相关性返回最该被记起的片段;- 注释里还写了「rebuildable FTS5 projection」——索引是可重建的投影,坏了能重建。
配合前面 facts / chapter_summaries / hooks 三张表和 inkos consolidate(卷级摘要),构成一套分层记忆:近期细节 + 卷级摘要 + 按需检索的历史事实。
6. 流水线:一章是怎么被「做」出来的
pipeline/runner.ts 里的 PipelineRunner 提供了一串阶段方法(真实方法名):
initBook / initFanficBook / initSpinoffBook / initImitationBook
planChapter → writeDraft → auditDraft → reviseDraft → writeChapters
reviseFoundation / importFanficCanon …
配套文件各管一段:
| 文件 | 作用 |
|---|---|
chapter-persistence.ts | persistChapterArtifacts:把一章的产物先在工作区落盘再提交 |
chapter-review-cycle.ts | 审阅循环(draft → audit → revise) |
chapter-truth-validation.ts | 提交前校验「状态与正文是否自洽」 |
chapter-state-recovery.ts | 状态退化时的恢复路径(对应 CLI 的 write repair-state) |
persisted-governed-plan.ts | 落盘版的「受治理计划」(输入治理产物) |
scheduler.ts | 守护进程(inkos up)的调度 |
short-fiction-runner.ts / script-storyboard-runner.ts | 短篇 / 剧本分镜等其它形态的流水线 |
原子提交的直觉:正文/状态/伏笔先在章节工作区里校验,通过了才提交——避免出现「状态已推进、正文没落盘」的撕裂。CLI 里的 write sync 与 write repair-state 就是这套机制的对外出口。
7. 审计 37 维:查的到底是什么
第 0 篇说「37 个维度」。源码 agents/continuity.ts 里是一张维度化名表,比如(真实条目):
37: { zh: "正典事件一致性", en: "Canon Event Consistency Check" },
也就是说:审计不是一个「你检查一下」的笼统提示,而是 37 个具名检查项的清单——角色记忆、物资连续性、伏笔回收、大纲偏离、叙事节奏、情感弧线……每一项都有自己的说明与升级策略(源码里还按语言、按题材做差异化提示)。
另外几个和「像不像 AI 写的」相关的模块也在 agents/ 下:ai-tells.ts(AI 痕迹特征)、detector.ts / detection-insights.ts(AIGC 检测与解读)、post-write-validator.ts(写后校验),对应 CLI 的 inkos detect 与 revise --mode anti-detect。
8. 输入治理与模型层(models/)
| 文件 | 管什么 |
|---|---|
input-governance.ts | 输入治理:控制文档优先、上下文选择策略 |
length-governance.ts | 字数治理:目标值 vs 允许区间(对应 --words 的真实语义) |
context-compression.ts | 上下文压缩 |
state.ts / runtime-state.ts | 状态的类型定义与运行时形态 |
book-rules.ts / genre-profile.ts / style-profile.ts | 本书规则 / 题材档案 / 文风指纹 |
detection.ts | 检测相关模型 |
这解释了第 0 篇里那些「看起来像玄学」的行为:字数为什么是「目标值」而不是硬截断(length-governance)、规则为什么要三层叠加(写手内置 + 题材 + 本书 book_rules)、文风指纹怎么注入(style-profile)。
9. Skills:只给指令,不给权限
skills/ 目录(registry / builtin-loader / external-loader / production-bindings)实现的是 SKILL.md 体系。关键设计:外部 Skill 只提供指令与静态资料,通过 skill-tool 暴露给 agent,但不会带来新的文件/网络/写权限——权限边界始终由工具白名单(第 3 节)决定。这也是第 0 篇强调的那条安全模型在代码上的位置。
10. 复盘:一条指令到一章落盘
11. 可借鉴的三条工程原则
- 模型提议,宿主执行——重动作走
propose_action+ 确认闸门;权限只通过工具白名单授予(agent-tools.ts就是权限清单)。 - 状态是「校验过的 JSON + 人看的投影」——
validator拒收坏数据、reducer只应用 delta、projections负责可读性;别让模型直接改「真相」。 - 完成以工具结果/落盘为准——
persistChapterArtifacts+ 章节工作区 + 版本归档,把「模型说写完了」变成「文件确实在、状态确实自洽」。
关联
- 前置:InkOS 入门、InkOS 深度体验
- 底层运行时:pi-agent 系列(
agent-session明确建在它上) - 同类思路对照:MCP(工具的协议化)、frontend-agent(手写工具循环)
自测
agents/和agent/两类 agent 的区别是什么?propose_action存在的意义是什么?权限由谁授予?- 状态为什么用「JSON delta + 校验 + 投影」而不是让模型直接写 markdown?
- 检索为什么用 FTS5 + BM25,而不是把历史全塞上下文?
- 一章「原子提交」想避免的坏情况是什么?
参考
- 仓库:
Narcooo/inkos(本地克隆版本 v1.8.0-4-g6e9979b4,2026-09-07) - 上架文档:InkOS 入门(同一仓库 v1.8.0 的安装/CLI 事实)
- 底层运行时:pi(
@mariozechner/pi-ai、pi-agent-core),见 pi-agent 系列 - 许可:AGPL-3.0