数据截至 (上游 commit 9fb509bab3e0)
DeepChat — 架构与原理
30 秒导读: DeepChat 是一个开源、本地 优先的 AI Agent 桌面客户端(Electron + Vue 3)。它能接 OpenAI/Gemini/Anthropic/Ollama 等模型,也能挂 MCP 工具、Skills、ACP 编码 agent,还能从 Telegram/飞书等远程控制。它最独特的设计是 Tape:把一次会话的全过程(消息、工具调用、结果、压缩、分叉)记成一条 append-only(只追加)的事件日志,需要展示给模型的「当前对话」是从这条日志 折叠(fold) 出来的——所以历史永远可恢复、可追溯、可重放。
1. 这是什么(零基础也能懂)
一句话定义: DeepChat 是一个装在你电脑上的「带手脚的聊天客户端」——它不只是把你的话发给大模型,还能让模型读写文件、跑命令、搜代码、调用 MCP 工具,并且把整个过程完整记录下来。
解决什么问题 / 给谁用:
假设你是工程师,想在本地用一个统一界面:既能跟云端 GPT/Claude 聊天,又能让它在你的项目目录里帮你改代码、跑测试;还希望换一台机器、关掉再打开,之前那段 agent 工作能原样恢复,而不是「聊天记录还在,但工具调用、被压缩掉的上下文全丢了」。DeepChat 就是为这个场景做的。
它能做什么(功能):
- 统一管理多家云模型 + 本地 Ollama(一个 app 不用来回切)
- 本地 agent 工具:读写文件、执行命令、
glob/grep搜代码、子 agent 编排、计划任务 - MCP(Model Context Protocol)工具:一键安装外部工具服务器
- Skills:按会话装载可复用的任务说明(代码审查、文档、前端等)
- ACP(Agent Client Protocol):把外部编码 agent 当成一个「模型」选项跑
- 远程控制:从 Telegram、飞书/Lark、QQ、Discord、微信 iLink 驱动会话
- 本地数据加密(SQLCipher)、会话搜索、上下文压缩、请求 Trace 预览
用起来什么样: 桌面里像普通聊天窗口,但模型回复中会穿插「工具调用块」(读了哪个文件、跑了什么命令、要不要授权),你可以批准/拒绝;每条消息还能点开 Trace 看这次真正发给 provider 的请求长什么样。
一句话直觉/类比: 把会话想成一盘磁带(Tape)——只能往后录(append-only),不能擦改;你看到的「当前对话」是放磁带时实时算出来的剪辑版。要发给模型的上下文,就是这盘磁带按预算裁出来的一段。这正是 DeepChat 标榜的 Tape.systems 哲学:keep the process(保留过程)。
本节不出现底层代码。目标:你现在应该能说清「DeepChat 是个本地 agent 客户端,卖点是把会话过程完整存成可恢复的磁带」。
2. 顶层全景(它大概怎么转)
DeepChat 是 Electron 应用,分三个世界:Renderer(Vue 界面)、preload bridge(安全 IPC 桥)、Main(Node 主进程,所有重活在这)。Main 里用 Presenter 模式——每个子系统一个 presenter 类持有 runtime 状态。
怎么读这张图:从上往下是一条用户消息的去向。Renderer 不直接碰 presenter,
中间隔着 typed route(类型化路由),真正的聊天 runtime 在 AgentRuntimePresenter。
┌─────────────────────────────────────────────┐
│ Renderer (Vue 3 / Pinia stores / views) │
│ renderer/api/*Client.ts ← 组件唯一入口 │
└───────────────┬─────────────────────────────┘
│ window.deepchat.invoke(route)
┌───────────────▼─────────────────────────────┐
│ preload bridge (createBridge.ts) │ contextIsolation on
└───────────────┬─────────────────────────────┘
│ shared/contracts 路由 + 事件契约
┌───────────────▼─────────────────────────────┐
│ Main: src/main/routes (typed dispatcher) │
│ SessionService / ChatService / ... │
└───────────────┬─────────────────────────────┘
│ 窄 port
┌───────────────▼─────────────────────────────┐
│ AgentSessionPresenter 会话生命周期/绑定窗口 │
└───────────────┬─────────────────────────────┘
│ 委托执行
┌───────── ──────▼─────────────────────────────┐
│ AgentRuntimePresenter ← 聊天 runtime 核心 │
│ ┌─ Tape (append-only 事件日志 → effective) │
│ ├─ contextBuilder (token 预算裁剪) │
│ ├─ processStream (统一 stream/tool 循环) │
│ └─ compaction / messageStore / trace │
└──────┬──────────────────────┬────────────────┘
│ │
┌──────▼────────┐ ┌──────▼─────────────────┐
│ ToolPresenter │ │ LLMProviderPresenter │
│ MCP + 本地 │ │ BaseLLMProvider. │
│ agent tools │ │ coreStream() + ACP │
└───────────────┘ └────────────────────────┘
部件一句话职责:
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
renderer/api/*Client | typed renderer 客户端,吸收 bridge/channel 细节 | src/renderer/api/ |
| shared contracts | route 注册表 + schema + typed 事件目录 | src/shared/contracts/ |
| preload bridge | 暴露 window.deepchat.invoke/on | src/preload/createBridge.ts |
| main routes | typed route 分发与 service | src/main/routes/index.ts |
AgentSessionPresenter | 创建/恢复/激活/分叉会话,绑定窗口 | src/main/presenter/agentSessionPresenter/index.ts |
AgentRuntimePresenter | 聊天 loop、stream、工具交互、持久化 | src/main/presenter/agentRuntimePresenter/index.ts |
DeepChatTapeService | Tape 服务边界:回填 facts、search/anchor/handoff/fork | src/main/presenter/agentRuntimePresenter/tapeService.ts |
ToolPresenter | 聚合 MCP + 本地 agent tools,路由调用 | src/main/presenter/toolPresenter/index.ts |
LLMProviderPresenter | provider 实例、model 管理、ACP helper、AI SDK runtime | src/main/presenter/llmProviderPresenter/index.ts |
主线走一遍(高层,不进代码):
- 用户在界面发消息 →
ChatClient→window.deepchat.invoke('chat...')→ main route。 - route 调
AgentSessionPresenter,后者把执行委托给AgentRuntimePresenter.processMessage()。 - runtime 先 ensureSessionTapeReady(确保磁带就绪并折叠出当前历史),用
contextBuilder按 token 预算裁出要发的上下文。 - 进入
processStream统一循环:调 provider 的coreStream拿流式事件;如果模型要调工具,就交给ToolPresenter执行,把结果接回去继续循环;需要授权时暂停,等用户响应。 - 流式过程实时 echo 给 renderer(
chat.stream.*事件);结束时把消息和工具结果作为 新的 facts 追加进 Tape,落库。
看懂这五步,就抓住了 DeepChat 的「大盘」。下面四章逐个拆。
3. 这套文档怎么读(阅读地图)
DeepChat 体量大,按下面顺序由浅入深:
| 章节 | 讲什么 | 何时读 |
|---|---|---|
| 01-tape-system.md | Tape:append-only 事件日志 + 折叠出 effective view。理解 DeepChat 必读的第一章。 | 想懂「为什么会话可恢复/可重放」 |
| 02-chat-loop.md | 一条消息端到端:上下文预算 → 统一 stream/tool 循环 → 压缩与持久化。 | 想追一次真实聊天链路 |
| 03-tool-system.md | 工具怎么聚合(MCP + 本地 agent tools)、怎么路由 、权限预检查与暂停交互。 | 想加工具 / 懂权限模型 |
| 04-provider-acp.md | 两层 Provider 抽象 + coreStream 标准化事件;ACP 把外部 agent 当「模型」。 | 想接新 provider 或懂 ACP |
4. 巧妙之处(一眼提炼)
- 「过程即真相」:不把 UI 上的聊天记录当唯一来源,而是把每一步记成只追加的 facts,展示用的对话是算出来的视图——这让压缩、分叉、子 agent 合并都成了「在日志上加一条 anchor」而非「破坏性改写」。详见 01。
- 统一 stream 循环:简单补全和多轮工具调用走同一段代码路径(
processStream),靠stopReason === 'tool_use'决定是否继续,而不是两套分支。详见 02。 - provider 无关的工具视图:
AgentRuntimePresenter只拿一个统一的MCPToolDefinition[],完全不知道某个工具是 MCP 还是本地——来源映射藏在ToolMapper里。详见 03。 - provenance-key 幂等去重:Tape 追加用
provenanceKey做唯一约束,重复回填同一条 fact 不会重复入库——这让「懒回填旧会话」安全可重入。详见 01。
5. 边界与局限(诚实)
- 重 Electron、重原生依赖:better-sqlite3、duckdb VSS、各平台 runtime 注入(
node/uv/rtk),不是「npm install 就能跑」的轻应用;package.json里大量installRuntime:*/plugin:bundle脚本印证了这点。 - Tape 的语义边界:当前 baseline 仍以
legacy_context_v1选择器折出上下文(docs/architecture/deepchat-tape-baseline/spec.md);ViewManifest 目前主要作为「解释与回归工件」,memory graph 检索、跨会话召回、CI 内 live replay 都是 deferred scope。 - 历史兼容残留:
SessionPresenter、legacy import、旧conversations/messages表仍保留,但只承担兼容职责,不在活跃聊天主链路上(见docs/ARCHITECTURE.md第 5 节)。
6. 横向对比(同 shelf 兄弟)
- 与「会话即数据库行」的传统聊天客户端不同,DeepChat 把过程而非结果当一等公民:别人存「最终消息」,它存「产生消息的事件流」,再折叠出消息。这与 agent 记忆/可观测方向的项目(把 trace 当训练/回放素材)同源。
- 与纯 MCP host 相比,DeepChat 把 MCP 工具、本地 agent 工具、ACP 外部 agent 三类异构能力统一到 同一个工具/模型选择面之下,屏蔽来源差异。
7. 代码地图(导航索引)
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 会话编排入口 | src/main/presenter/agentSessionPresenter/index.ts | AgentSessionPresenter |
| 聊天 runtime 核心 | src/main/presenter/agentRuntimePresenter/index.ts | AgentRuntimePresenter、processMessage |
| 统一 stream/tool 循环 | src/main/presenter/agentRuntimePresenter/process.ts | processStream |
| Tape 服务边界 | src/main/presenter/agentRuntimePresenter/tapeService.ts | DeepChatTapeService、ensureSessionTapeReady |
| Tape 折叠(effective view) | src/main/presenter/agentRuntimePresenter/tapeEffectiveView.ts | buildEffectiveTapeView |
| Tape facts 写入 | src/main/presenter/agentRuntimePresenter/tapeFacts.ts | appendMessageRecordToTape、appendToolFactsToTape |
| Tape 存储表 | src/main/presenter/sqlitePresenter/tables/deepchatTapeEntries.ts | DeepChatTapeEntriesTable、append |
| 上下文预算 | src/main/presenter/agentRuntimePresenter/contextBuilder.ts | buildContextWithMetadata、selectTurnHistoryTurns |
| 工具聚合/路由 | src/main/presenter/toolPresenter/index.ts | ToolPresenter、getAllToolDefinitions |
| 本地 agent 工具 | src/main/presenter/toolPresenter/agentTools/agentToolManager.ts | AgentToolManager |
| Provider 抽象基类 | src/main/presenter/llmProviderPresenter/baseProvider.ts | BaseLLMProvider、coreStream |
| ACP provider runtime | src/main/presenter/llmProviderPresenter/acp/ | acpSessionManager、acpProcessManager |