首页/架构/系统架构

系统架构

这一页讲 Trove 的分层与边界:代码分成哪几块、每块的职责是什么、 为什么有些看起来该放一起的东西被刻意分开了。具体到「一次提问经过了哪些节点」 在查询工作流。

全貌

先把形状看一眼,再往下读细节。图上的两个带(入口、能力)夹着编排——它是一张 LangGraph 图,所以画成一个节点;加外挂的模型层与状态层,就是下面「五层」那张 表的形状。数据源在 Trove 之外,所以不在这五层里;行动的执行同理—— 判定触发产出的提案、审批与回执在系统内,执行在外(webhook 推 / MCP 拉)。 实线是「谁调谁」,虚线是「谁会用到大模型」;两条都不是数据流。

flowchart TB
    %% 配色 = 站点 token 的半透明版(zinc 中性 + indigo 主色 + 链路各段一色):
    %% 填充一律低透明度、描边用实色、文字不动色——亮暗两档同一份源码都成立
    %% (mermaid 把颜色烘进 SVG,跨主题只有透明度能自适应)。
    subgraph entry["入口"]
        UI["Web UI"]:::entry
        SRV["HTTP 服务"]:::entry
        CLI["CLI / REPL"]:::entry
        MCP["MCP"]:::entry
    end

    WF["编排 · trove/workflow<br/>LangGraph 工作流<br/>语义门禁 → 计划与编译<br/>生成 → 执行与校验 → 反思"]:::core

    subgraph caps["能力 · trove/services"]
        SEM["语义模型"]:::cap
        ANA["分析 · 归因分解 · 统计器械"]:::ana
        KB["知识库 + 混合检索"]:::cap
        MEM["记忆 · 判定规则 · Skills"]:::cap
        ACT["行动 · 提案 / 审批 / 回执 / 效果验收"]:::act
    end

    LLM["模型 · trove/llm<br/>LLM 网关"]:::base
    DS["数据源<br/>PostgreSQL · MySQL · Doris<br/>ClickHouse · DuckDB · SQLite<br/>Snowflake · BigQuery"]:::ext
    STATE["状态 · trove/storage<br/>PostgreSQL / SQLite<br/>会话 · 任务 · 检查点<br/>查询日志 · 谱系"]:::base

    UI --> WF
    SRV --> WF
    CLI --> WF
    MCP --> WF
    WF --> SEM
    WF --> ANA
    WF --> KB
    WF --> MEM
    MEM -->|"判定触发"| ACT
    WF -.-> LLM
    KB --> DS
    MEM --> STATE

    classDef entry fill:#71717a14,stroke:#71717a59,stroke-width:1px;
    classDef core fill:#6366f11f,stroke:#6366f1,stroke-width:2px;
    classDef cap fill:#6366f114,stroke:#6366f199,stroke-width:1.5px;
    classDef ana fill:#14b8a614,stroke:#14b8a699,stroke-width:1.5px;
    classDef act fill:#8b5cf614,stroke:#8b5cf699,stroke-width:1.5px;
    classDef base fill:#71717a14,stroke:#71717a59,stroke-width:1.5px;
    classDef ext fill:transparent,stroke:#71717a80,stroke-width:1.5px,stroke-dasharray:4 4;
    style entry fill:#71717a0d,stroke:#71717a2e,stroke-width:1px
    style caps fill:#71717a0d,stroke:#71717a2e,stroke-width:1px
    linkStyle 8 stroke:#f59e0b,stroke-width:1.5px
图 1 · 入口进来,编排决定做什么,能力层干活(问数 · 分析 · 判定 · 行动都在这一层),落点在状态与数据源;行动的执行在系统外。颜色按层读:编排 indigo、分析 teal、行动 violet、系统之外 zinc 虚线、底座中性;琥珀色的边是「判定触发」。 上面这段以源码显示:图渲染库没加载起来(离线、CDN 被拦或浏览器禁用了 JavaScript 时会这样)。这段 Mermaid 文本本身就说明了上面那些框与连线。

五层

层目录职责
接口层 trove/api/ HTTP 路由、鉴权依赖、SSE 流式输出
工作流层 trove/workflow/ LangGraph 图、节点、确定性规则链、版本比对
服务层 trove/services/ 知识库、语义模型、检索、记忆、判定、行动、分析、Skills、数据源、任务调度、认证与授权
模型层 trove/llm/ litellm 网关、agent 循环骨架、追踪
存储层 trove/storage/ 内部状态的统一后端抽象与版本化迁移

值得说明的是服务层与工作流层的关系:工作流层不含业务知识, 它只负责「按什么顺序、带什么上下文去调用服务」。所以同一个 KbService, 对话链路、评测脚本、MCP 通道都在用,而它们的编排方式各不相同。

多个入口,一张图

进程入口有三个:REPL / CLI(trove、trove-cli)、 HTTP 服务(trove serve,Web UI 走的是它)与 MCP(trove mcp)。它们最终都汇到同一张 LangGraph 图上—— 图不是在每个入口各建一份,而是由统一的工厂装配。

main.py:119 create_app_components graphs.py:1402 build_graphs graphs.py:2109 _build_reflection main.py:382 build_graphs

为什么「28 个节点」这个数字要较真

build_graphs 的开关默认值并不一致:agentic、multi_candidate、query_sketch 默认开,只有 clarify 默认关。生产调用只传了 checkpointer,其余全取默认值——所以对外报的节点数必须是默认口径下的那张图。

把 clarify 算进去会得到 29。它确实存在,但只在显式开启时挂上;反过来,把某个默认开的开关当成关,报出来的数也会偏低。读者照着自己跑一遍,看到的应该是 28。

服务的装配与注入

节点是工厂函数:make_xxx(services...) -> async def node(state)。 依赖在构图时被绑进闭包,运行期节点不需要去任何全局注册表里找东西。 这样做的好处很直接:测试可以给某个节点单独注入假的 LLM 或假的连接器, 而不必启动整个系统。

服务集合本身是一个 GraphServices 对象,由 main.py 在启动时装配。 图拿到的是已经构造好的实例,而不是构造参数——装配逻辑与流程逻辑因此可以分开演进。

一次提问的数据流

从高一层看,一次问答经过这几个阶段(细节在查询工作流):

阶段产出是否用 LLM
意图路由 判定这是问数 / 元数据 / 闲聊 / 纠错 / 写操作… 是
语义链接 这个问题涉及哪些表(matched_tables) 检索为主,确定性过滤为辅
快径尝试 命中模板则直接出 SQL 否
计划与编译 查询计划 → 权威 SQL 或骨架 计划用,编译不用
生成 SQL(agentic 循环,带工具自校验) 是
执行与校验 结果集 + 规则链判定 否
反思与回滚 判定这一轮是否进步,决定重试还是回退 是(判定有确定性版本比对兜底)
解释与交付 人话解释、图表、结论 是

这张表里「是否用 LLM」那一列是这套架构的核心信息:越靠近正确性判断的环节, 越不该用 LLM。结果对不对由规则链判、这一轮有没有变好由版本比对判—— 只有理解和表达交给模型。

内部状态:一个抽象,两档后端

会话、任务、用户事实、记忆、认证、设置、任务调度、查询日志—— 这些东西此前散在多个 SQLite 文件里,现在统一到一个 StorageBackend 抽象后面。

另有两处是另一回事——不是被排除,而是机制本来就不同: 图检查点用 LangGraph 自带的 saver(认同一个 TROVE_STORAGE_URL 开关,但不经过这套 store 代码), 血缘库是服务自管的一份 SQLite 镜像。 两处的后端选择跟着同一个开关走,代码路径却各是各的。

sqlite.py:138 build_backend sqlite.py:166 resolve_backend checkpoint_store.py:38 build_checkpointer service.py:122 lineage.sqlite

场景后端怎么选中
生产 PostgreSQL 设了 TROVE_STORAGE_URL
本地 / 测试 SQLite 未设该变量时的兜底

两个后端共用同一套 store 代码,靠的是后端自己做 SQL 方言翻译: ? 占位符转 %s、AUTOINCREMENT 转 IDENTITY、需要 lastrowid 时改写成 INSERT ... RETURNING id。这让上层 store 不必写两份 SQL。

另外两件刻意「不上」这个抽象的东西

KB 镜像与业务数据源适配器留在外面。前者依赖 FTS5 虚拟表(PostgreSQL 生产走 pg_hybrid 检索后端,是另一条路);后者是用户的数据,本来就不该被 Trove 的内部存储抽象管。

也就是说:这个抽象管的是「Trove 自己的状态」,不是「所有数据库访问」。

迁移是版本化的,并且有两条不变量:每个操作各自幂等(这才让「从 0 重放」 与「存量库接入」走同一条代码路径),以及版本号反映历史而非配置。 第二条的意思见混合检索里那个退役列的例子。

数据源:一个注册表,驱动按需加载

数据源适配器由一张注册表管理。驱动是懒导入的—— 导入注册表本身不需要装任何 extras,只有真正去连某个类型的库时才要求对应的驱动包。

registry.py:42 _ADAPTER_REGISTRY

所以「Trove 支持哪些数据源」的答案不是写死在文档里的一句话,而是注册表的 key 集合 (检查器会数它)。新增一个方言的工作量集中在实现 DatabaseAdapter 的几个方法: 连接、执行、取 schema、上报能力、声明方言。

接口层

HTTP 服务由工厂函数构造,OpenAPI 文档挂在 /v1/docs—— 也就是说接口清单有一份活文档,不必手抄进站点。 这也是API 参考只给分类清单与场景的原因。

app.py:325 create_app app.py:334 FastAPI app.py:473 health

鉴权是 fail-closed 的:没有认证组件时服务拒绝启动,而不是「裸奔运行并打条警告」。 这条规则在安全边界里展开。

继续读

想了解去哪
28 个节点逐个讲 + 分支与回滚查询工作流
产品功能怎么分块、对谁开放、边界在哪功能架构
五层里的服务层具体有什么数据能力 / Agent 能力
部署形态与容器拓扑安装部署
存储后端的配置项配置参考
本页源码锚点的基准提交:anchor-base: 7c6df87 GitHub 仓库 文档首页