WRITING / 2026.09.19

Tritree 深度拆解:把一次性 AI 写作改造成可回溯的创作树

从创作树状态模型、AI Director、流式协议、产物插件到 SQLite/MySQL 持久化,拆解 Tritree 如何把用户选择放回 AI 创作闭环。

Tritree 解决的是一个很具体的创作问题:很多 AI 工具让用户输入一句话,然后等待一份“看起来已经完成”的结果。结果不满意时,用户只能继续写提示词,或者整段重来。创作过程中的判断被压缩成了一次请求,用户也很难知道自己为什么走到了当前版本。

Tritree 的做法是把创作拆成连续的选择。用户先提供一个 seed,系统生成初始产物和下一轮的三个方向;用户选择其中一个方向,树上就增加一个子节点,AI 再根据当前路径继续生成。没有被选择的方向会被折叠,但不会消失,之后可以回到历史节点重新分支。

这不是把聊天窗口换成了树形 UI,而是把“创作过程”变成了一组可以持久化、校验和回放的状态。本文按当前仓库主分支源码进行拆解,源码事实与架构判断分开说明。文中的实现链接指向 qdaxb/tritree 的公开代码;仓库持续变化时,细节应以对应版本为准。

先看完整链路:一次选择如何变成一棵树

从用户点击一个方向,到下一版产物出现在编辑器里,大致经过下面几层:

Seed 与创作要求
      │
      ▼
Root Memory ── 作品类型、风格、Skills
      │
      ▼
Session ── 当前作品与当前节点
      │
      ▼
TreeNode ── 本轮意图、三个方向、选中方向、折叠方向
      │
      ├── AI Director / Mastra Agent
      │       ├── 生成结构化产物
      │       └── 生成下一轮三个方向
      │
      ▼
Artifact ── social-post 或 prd 的版本化内容
      │
      ▼
流式事件 ── 产物、思考过程、选项、完成状态
      │
      ▼
编辑、对比、继续分支或回到历史节点

前端的 TritreeApp 负责协调加载、生成、选择、历史分支和移动端状态;TreeCanvas 负责树的可视化;ArtifactWorkspace 负责产物渲染、编辑、Diff 和生成过程展示。服务端 API 按会话、节点和产物拆开,数据库仓储层再把一次操作组合成完整的 SessionState

这种分层带来一个重要效果:AI 只负责提出内容和下一步建议,当前节点是什么、谁拥有这个会话、哪个方向已经被选过、哪些版本可以比较,仍由应用状态和数据库决定。

创作树不是一张图,而是一组状态约束

核心领域类型集中在 src/lib/domain.ts。其中最重要的四个对象是 SessionTreeNodeArtifactFoldedBranch

Session 是一件作品的容器,记录作品类型、标题、状态和 currentNodeIdTreeNode 记录一次 AI 决策或一次产物生成,包括:

  • parentIdparentOptionId:它从哪个节点、哪个方向进入;
  • roundIndexroundIntent:当前是第几轮,AI 认为本轮要解决什么问题;
  • options:本轮可以选择的方向;
  • selectedOptionId:用户最终走了哪一个方向;
  • foldedOptions:同一轮没有选择、但仍可回看的方向;
  • producedArtifactId:该节点是否产生了正式产物;
  • agentMessages:与本轮 Agent 执行相关的消息轨迹。

Artifact 不直接塞进节点 JSON,而是单独存储,包含类型、版本、payload、来源产物和创建节点。这样,节点负责描述“为什么走到这里”,产物负责描述“这里生成了什么”。一个后续版本还可以通过 sourceArtifactIds 指向上一个版本,形成内容血缘。

领域层对这些关系做了几道硬约束。一个 artifact 节点必须有 producedArtifactId;非产物节点不能伪造产物 ID;AI 选项必须刚好三个,并且 ID 必须是 abc 各一次。选项还必须带有 labeldescriptionimpactkind,其中 kind 用来表达探索、深化、重构或收尾意图。

这些约束的价值在于,树不是“前端画出来像一棵树”就算成立。即使模型返回了四个选项、重复了一个 ID,或者在分析节点上附带了产物 ID,状态也会在进入仓储层前被拒绝。用户界面因此可以依赖一个稳定协议,而不是猜测模型输出。

三选一是交互设计,也是搜索空间控制

Tritree 把每轮选择限制成三个方向,看起来像一个产品交互决定,实际上也在控制 AI 创作的搜索空间。

如果每轮都让模型返回任意数量的建议,前端要处理数量不定的布局,用户也要面对越来越大的选择成本。固定三个方向后,每轮的认知负担有上限,协议、测试和 UI 都更简单。源码通过 DirectorOptionsOutputSchemaDirectorNextStepOutputSchemaDirectorTurnOutputSchema 反复校验这个约束。

但三个方向并不意味着三个同质按钮。每个选项有 kindmode:可以是继续探索、加深当前角度、重新框定问题或结束创作;生成模式还可以区分 divergentbalancedfocused。因此,三个按钮表达的是三个不同的下一步,而不是同一提示词的三个措辞版本。

这也解释了为什么历史分支需要一等数据模型。用户没有选择的方向不能简单丢弃,否则每次选择都会破坏探索记录。仓储层的 saveNodeSelection 会把选中的方向写入 selected_option_id,把其他方向写入 folded_options_json,同时在 branch_history 中留下记录。

当用户从历史节点再次选择一个方向时,activateHistoricalBranch 会优先查找已经存在的子节点。如果找到,就直接激活已有分支;如果没有找到,才创建新的子节点。这个行为避免了用户重复点击历史方向时不断生成相同的数据库分支,也让“回到过去”既可以复用结果,也可以从过去重新生长。

生成流程:AI Director 被放在严格的输出协议里

Tritree 没有把所有 AI 行为塞进一个通用聊天接口,而是按任务拆出了几类 Agent:

  • Tree Artifact Agent:生成或更新正式产物;
  • Tree Options Agent:为当前节点生成下一轮三个方向;
  • Tree Next Step Agent:判断下一步应该继续生成产物、让用户选择,还是结束;
  • Main ReAct Agent:在需要工具、子代理或过程材料时,完成一轮完整执行。

这些 Agent 由 src/lib/ai/mastra-agents.ts 创建,统一接入 Mastra、AI SDK provider、上下文预算和 TokenLimiter。提示词在 mastra-context.tsprompts.ts 中分层构造,包含初始输入、当前产物、已选路径、折叠分支、Root Memory 和当前启用的 Skills。

结构化输出先于业务落库

一个典型的选项输出不是自由文本,而是类似下面的对象:

{
  "roundIntent": "把主题从经验分享收束到可执行的工程方法",
  "options": [
    {
      "id": "a",
      "label": "补充故障场景",
      "description": "从一个真实问题切入",
      "impact": "增强具体性和可信度",
      "kind": "deepen"
    },
    {
      "id": "b",
      "label": "改成方法论结构",
      "description": "围绕原则、步骤和边界重组",
      "impact": "更适合技术读者复用",
      "kind": "reframe"
    },
    {
      "id": "c",
      "label": "压缩成短帖",
      "description": "保留结论并减少铺垫",
      "impact": "更适合社交平台发布",
      "kind": "finish"
    }
  ]
}

源码事实是:Zod schema 会检查字段类型、选项数量和 ID 集合;解析失败时,执行器会保留原始错误并在必要时修复字符串中的未转义换行。这个修复只解决 JSON 表面格式,不能把语义错误变成正确答案。真正的业务状态仍要经过 artifact plugin 的 payload schema 再校验。

ReAct 的终点是工具提交

在需要运行 MCP 工具或子代理时,Agent 不应该先调用工具、再把一段看似完整的 JSON 作为普通文本输出。Tritree 为不同任务提供 submit_tree_artifactsubmit_tree_optionssubmit_tree_next_step 等最终提交工具,并要求 Agent 在调用后立即停止普通文本输出。

这是一种很实用的边界:工具调用属于过程,最终提交属于协议。执行器可以在最终工具参数上做 Zod 校验,再把结果写入数据库;前端也可以区分“AI 正在工作”和“本轮结果已经确定”。

Provider 选择是配置问题,不是领域逻辑

director.ts 把模型 provider、模型名、Base URL 和认证 token 的选择集中处理。可以显式设置 TRITREE_AI_PROVIDER=openai,也可以根据环境变量在 OpenAI 与 Anthropic-compatible 接口之间自动选择;OpenAI 还支持 Responses API 和 Chat Completions 两种模式。

这种设计把“使用哪家模型”从创作树领域逻辑中拿掉了。Agent 只依赖一个 language model 工厂,三选一协议、产物类型和状态落库不会因为 provider 改变而改变。

流式输出解决的是可见性问题

生成一份社媒内容或 PRD 可能需要较长时间。Tritree 的接口不是等模型全部完成后才返回 JSON,而是通过 artifact/generate/stream 返回 NDJSON 事件。当前路由定义了几类事件:

artifact.replace  当前产物发生替换
thinking         思考或执行阶段文本
process_data     工具产生的可展示材料
options          新一轮三个方向
done             返回完整 SessionState
error            返回可展示的错误

TritreeApp 逐条消费这些事件,更新流式产物、思考面板、过程材料和树上的选项。用户可以在生成过程中看到当前阶段,也可以停止生成;桌面端和移动端还分别处理产物区域的自动滚动。

这里有一个容易被忽略的设计点:流式事件只是 UI 传输格式,最终状态仍以数据库中的 SessionState 为准。生成结束后,服务端发送 done 和完整状态,前端用它收敛本地状态。这样,局部增量渲染不会成为另一份事实来源。

Artifact Plugin:把“生成内容”变成可扩展协议

Tritree 当前内置 social-postprd 两个产物插件。插件接口定义在 src/artifacts/types.ts,服务端注册表在 src/artifacts/registry.ts。一个插件需要提供:

  • payload schema 和 AI output schema;
  • 从 seed 创建初始 payload 的方法;
  • 给 Agent 的提示词指令;
  • 产物规范化、树节点摘要和 Director 摘要;
  • 是否支持生成、编辑、Diff、交付以及哪些流式字段;
  • 可选的产物动作处理器。

社媒插件的 payload 是 titlebodyhashtagsimagePrompt,还提供选中文本局部改写动作。PRD 插件的 payload 是 titlemarkdown,交付时可以生成 Markdown 文档。两者共享会话、树、AI Director 和数据库,却可以拥有不同的编辑器、渲染器和交付方式。

这条边界比“给所有产物留一个 JSON 字段”更有价值。它把变化拆成三种:

  1. 领域不变部分:会话、节点、分支、版本和权限;
  2. 产物协议部分:payload、摘要、提示词和动作;
  3. 界面交互部分:编辑器、渲染器、Diff 和发布面板。

如果未来新增代码评审报告、会议纪要或知识卡片,理想路径是增加一个插件,而不是让 TritreeApp 到处出现新的类型判断。这里的“理想”是架构推导,当前仓库已经证明的是两个内置插件遵守了这条接口边界。

Root Memory 和 Skills:把个人偏好放进上下文,但不让它接管状态

Tritree 有两层长期信息。

Root Memory 保存作品类型、seed、创作要求,以及领域、语气、风格和角色偏好;它还保留 summarylearnedSummary。创建新作品时,这些信息会被整理为 Director 输入的一部分。

Skills 是更细的可复用规则。它有分类、描述、完整 prompt、是否默认启用、是否延迟加载、父 Skill 和适用目标。适用目标可以是 writer、editor 或 both。默认 Skills 来自 .tritree/defaults.json,用户也可以在界面中管理、归档和导入 Skill。

模型上下文大致按以下顺序拼接:

作品类型与产物协议
      + 初始 Seed 与创作要求
      + 当前可见产物
      + 已选路径与折叠分支摘要
      + Root Memory 的总结
      + 本轮启用的 Skills
      + 当前用户选择或请求

这种设计把“用户偏好”和“当前会话状态”分开:偏好可以跨作品复用,会话路径只属于当前作品。推导上的风险也很明确:Skills 越多,上下文越长,规则之间冲突的机会越高;因此源码支持延迟加载、适用目标过滤和模型上下文预算限制。Skills 可以影响生成方向,但不能绕过会话所有权、节点关系和 payload 校验。

MCP 与子代理:扩展执行能力,同时保留最终边界

Tritree 可以从 .tritree/mcp.json 加载 stdio 或 HTTP MCP server,支持环境变量占位、超时、禁用项和诊断信息。运行时会把可用工具汇总给 Agent,并对工具调用数量设置上限。Agent 还可以按模板运行子代理,用于检索、整理材料或执行专项分析。

值得关注的不是“能接多少工具”,而是工具结果如何回到用户视野。show_process_data 工具要求把新工具产生的材料整理成通用展示结构,并为外部来源附上可点击 URL。它与最终的三个选项分开:工具材料用来支持用户理解和决策,不能偷偷变成另一组三选一。

这让系统保留了两条边界:外部工具可以扩展事实获取和处理能力,但不能直接修改核心树状态;只有最终提交工具的结构化结果,才会进入产物或选项更新流程。

数据层:SQLite 起步,MySQL 作为部署选项

数据库结构位于 src/lib/db/schema.ts,MySQL 对应结构位于 mysql-schema.ts。默认使用本地 SQLite 文件,也可以通过 TRITREE_DATABASE_URL 切换 MySQL;Drizzle 负责两种方言的 schema 映射和查询组合。

主要表之间的关系可以概括为:

users
  ├── root_memory
  ├── skills
  └── sessions
        ├── session_enabled_skills
        ├── tree_nodes ── parent_id 自关联
        │     └── produced_artifact_id
        ├── artifacts
        └── branch_history

一次“选择方向并创建子节点”不是先更新父节点、再单独插入子节点的两次请求。仓储层的 createArtifactChild 会检查会话和父节点归属,解析自定义选项,把父节点选中方向与新节点写入同一事务;saveNodeSelection 同时更新折叠方向和历史分支。

产物更新、选项更新和节点完成也使用事务。事务解决的是数据库写入的原子性:如果产物插入成功但节点状态更新失败,整次操作可以回滚。它不能解决模型重复生成、用户重复点击或网络重试带来的所有问题,所以 API 仍然需要检查当前节点、会话状态和已存在的历史子节点。

认证与自托管边界

API 路由在读取会话和写入状态前调用 requireCurrentUser。仓储查询同时携带 userId,会话、Root Memory、Skill 和管理接口都以用户归属为过滤条件。认证层支持 NextAuth Credentials 和 OIDC;首次没有用户时,README 规定第一个完成初始化的用户成为管理员。

这说明 Tritree 的“本地优先”不是把鉴权完全删掉,而是把默认存储和运行环境设得更接近单机应用,再保留面向团队的用户、管理员和 OIDC 扩展点。自托管时仍需要配置 NEXTAUTH_SECRET,并为 SQLite 文件或 MySQL 做备份。

仓库 README 当前将 Node.js 运行要求写为 >=24.0.0,而 package.json 的 engines 字段写为 >=22.0.0。这是源码快照中一个需要部署者自行核对的文档差异,不能把 README 的快速启动成功等同于所有环境都已经经过验证。

这套架构真正解决了什么

从产品角度看,Tritree 把“生成质量不满意”拆成了多个更容易回答的问题:是方向错了,还是结构不够深,还是表达方式不适合平台?用户不必一次写出完整提示词,而是在关键路口做小决策。

从工程角度看,它把 AI 的不确定性限制在几个受控接口中:

  • 模型负责提出文本和候选方向;
  • Zod 负责结构和数量约束;
  • Artifact Plugin 负责产物 payload 的合法性;
  • Repository 负责用户归属、事务和历史关系;
  • 前端流式协议负责把过程状态呈现给用户;
  • SessionState 负责在生成结束时重新收敛事实。

因此,Tritree 的核心创新不只是“把创作画成树”,而是让用户选择成为持久化状态,让 AI 的每次输出都成为下一次有边界的输入。

仍然需要警惕的代价

分支数量会自然增长

每个节点都可能产生三条方向,用户还可以从历史节点重新分支。树的可回溯性越强,存储、渲染和比较的成本就越高。当前实现通过摘要、折叠分支和局部视图控制界面复杂度,但仓库没有给出大规模树的性能基准,也不能据此推断深度或节点数量上限。

上下文会随着创作历史变长

Root Memory、Skills、当前产物、路径摘要、Agent 消息和工具结果都有可能进入上下文。TokenLimiter 能控制预算,却会带来信息截断或摘要损失。长期运行时,需要进一步定义哪些历史必须保留、哪些内容只存数据库、哪些摘要可以重新生成。

结构正确不等于内容正确

模型返回了合法的 a/b/c 和合法的 payload,只能说明协议通过。它仍可能给出重复角度、空泛建议或不符合平台语气的内容。当前源码提供结构校验和局部改写能力,但没有在 README 或代码中证明一套跨模型的质量评估体系,也没有公开吞吐、延迟或成本基准。

工具越多,审计越重要

MCP 和子代理扩展了 Agent 能力,同时增加了外部依赖、凭证、超时和结果可信度问题。工具结果需要被展示、记录和限制;如果未来允许工具直接修改更多业务状态,就需要更明确的授权、幂等和审计协议。

适合复用的设计原则

如果要在自己的 AI 应用中借鉴 Tritree,最值得复用的是下面几条边界:

  1. 把用户选择当作领域事件保存,不要只把它写进聊天记录。
  2. 为模型输出设计严格 schema,让错误尽早停在 AI 边界。
  3. 把产物类型做成插件协议,不要让核心页面知道每种内容的所有字段。
  4. 把过程流和最终状态分开,流式 UI 可以增量展示,但最终结果要重新从持久化状态收敛。
  5. 用事务保存父节点选择和子节点创建,避免树的边和节点出现半写入状态。
  6. 让工具扩展停留在执行层,通过显式的最终提交协议进入业务状态。

这些原则并不要求每个应用都使用三选一。短任务可以使用线性流程;开放式研究可能需要更多类型的节点;高风险领域还需要人工审核和更强的权限模型。Tritree 提供的是一种清晰的取舍:用有限的选择换取可解释的路径,用结构化协议换取 AI 行为的可控边界。

结语

一次性生成把用户放在结果的末端,Tritree 把用户放回每个关键转折点。这个转变要求系统同时保存内容、选择、路径、上下文和版本,也要求 AI 从“回答问题的聊天模型”变成“提出下一步、等待选择、提交结构化结果的导演”。

从源码看,创作树只是外显形状,真正支撑它的是一组稳定的约束:节点关系可追溯,方向数量可验证,产物类型可扩展,数据库写入有事务,流式过程与最终状态分离。对于希望构建长期运行的 AI 应用来说,这些边界比再增加一个生成按钮更值得研究。