数据截至 (上游 commit 9fb509bab3e0)
两层 Provider 抽象 + ACP 集成
本章一句话: 聊天循环只认一个方法
coreStream——不管背后是 OpenAI、Anthropic、Ollama,还是一个跑在子进程里的外部编码 agent(ACP)。
1. 它要解决的小问题
模型来源五花八门:OpenAI/Anthropic/Gemini 各家 SDK 不同;Ollama 是本地;GitHub Copilot 有自己的设备流认证;ACP agent 干脆是另一个进程。聊天 runtime 不可能为每家写一套。DeepChat 用两层抽象收口:上层 LLMProviderPresenter 管实例/选择,下层每个 provider 实现统一的 coreStream。
2. 思路:统一的 coreStream 契约
所有 provider 继承抽象基类 BaseLLMProvider(baseProvider.ts:39),它定义了一组必须实现的方法。聊天主链路只用其中一个:
baseProvider.ts:711-728
/** [新] 核心流式 处理方法
* 此方法由具体的提供商子类实现,负责单次API调用和事件标准化。
*/
abstract coreStream(
messages: ChatMessage[],
modelId: string,
modelConfig: ModelConfig,
temperature: number,
maxTokens: number,
tools: MCPToolDefinition[]
): AsyncGenerator<LLMCoreStreamEvent>
注意它的契约边界:coreStream 只负责「一次 API 调用 + 把响应标准化成 LLMCoreStreamEvent 流」。多轮工具循环不在这里——那是 02-chat-loop.md 的 processStream 干的。基类注释把这点写死了:completions 那种一次性方法「不进行工具调用(工具调用仅在 stream 版本中处理)」(baseProvider.ts:664)。
这就是分层的妙处:
processStream (runtime 层) ← 多轮循环、工具执行、暂停、Tape
│ 反复调用
▼
coreStream (provider 层) ← 只管单次调用 + 标准化事件
│ 各家自己实现
▼
OpenAI / Anthropic / Ollama / Copilot / ACP
3. 上层:LLMProviderPresenter
LLMProviderPresenter(index.ts:97)是 provider 的「总机」:
getProviderInstance(providerId)按 id 拿到具体 provider 实例(index.ts:204),实例由providerInstanceManager管。- 文本生成、image/video 生成、TTS、音频转写都从这里分发,但都最终落到某个 provider 实例的方法(例如 image generation 走
provider.coreStream(..., type: ModelType.ImageGeneration),index.ts:494-498)。 ModelType当前涵盖 chat、embedding、rerank、imageGeneration、videoGeneration、tts(docs/ARCHITECTURE.md第 4 节)——多模态能力复用同 一套 provider runtime,不绕开它。
具体 provider 实现都在 providers/ 下:aiSdkProvider.ts(走 Vercel AI SDK,覆盖 OpenAI/Anthropic/Google 等)、ollamaProvider.ts、githubCopilotProvider.ts、voiceAIProvider.ts、acpProvider.ts。AI SDK 那条还单独有一层 aiSdk/(messageMapper、providerFactory、streamAdapter、toolMapper……)把各家 SDK 差异再抹一层。
4. ACP:把外部 agent 当成「模型」
ACP(Agent Client Protocol)是 DeepChat 最有意思的集成:一个外部编码/任务 agent(可能是另一个 CLI 进程)被包装成一个 provider,在模型选择器里像普通模型一样选。
它为什么需要特殊处理?因为「调一个 agent 进程」远比「调一个 HTTP API」复杂——要管子进程生命周期、要维护 agent 侧会话、要把 DeepChat 的工具/MCP 配置翻译成 agent 认得的格式、还要把 agent 的权限请求接回 DeepChat 的暂停机制。这些都在 provider 层的 acp/ 目录:
| ACP 组件 | 职责 | 文件 |
|---|---|---|
| 进程管理 | 启动/管理 agent 子进程 | acpProcessManager.ts |
| 会话管理 | 维护 ACP 侧 session | acpSessionManager.ts、acpSessionPersistence.ts |
| 配置状态 | session 级配置选项 | acpConfigState.ts |
| 内容映射 | ACP 消息 ⇄ DeepChat 块 | acpContentMapper.ts、acpMessageFormatter.ts |
| 文件/终端 | agent 的 fs / terminal 能力 | acpFsHandler.ts、acpTerminalManager.ts |
| MCP 配置转换 | 把 DeepChat 的 MCP 配置喂给 agent | mcpConfigConverter.ts、mcpTransportFilter.ts |
| 能力 | 协商 agent 能力 | acpCapabilities.ts |
准备一个 ACP 会话的链路(docs/FLOWS.md 第 5 节):
Renderer ─ ensureAcpDraftSession(agentId, projectDir)
▼
AgentSessionPresenter ─ initSession(providerId='acp')
▼
LLMProviderPresenter.prepareAcpSession(sessionId, agentId, workdir) (index.ts:928)
▼
ACP helpers ─ 拉起进程 / 建会话 / 落配置 → ACP session ready
注意一个安全细节:远程控制创建 ACP 会话时,会用 channel 的 defaultWorkdir 或全局默认项目路径,并拒绝没有 workdir 的 ACP 默认 agent(docs/FLOWS.md 第 5 节)——外部 agent 不给它工作目录就不让跑。
ACP 的授权也走统一暂停:它的权限以 event.type === 'permission' 形式出现在 coreStream 流里,被 processStream 插成 pending 授权块(02-chat-loop.md 第 4 节,process.ts:493-506)——所以 ACP agent 想写文件,你在 DeepChat 里一样会看到「批准/拒绝」。
5. 巧妙之处
- 薄契约:coreStream 只承诺「单次调用 + 标准事件」,把多轮编排留给 runtime,使得「接一个新模型」=「实现一个 generator」,而不必懂 agent 循环。
- agent-as-model:把一个外部进程伪装成 provider,让 ACP agent 复用整套 UI / Tape / 权限,而不是另开一条平行通道。
- 多模态复用同一层:image/video/tts 不绕过 provider runtime,而是用
ModelType区分走同一个 coreStream,降低分叉。
6. 边界与局限
- ACP agent 的能力取决于它自己实现了多少 ACP 协议(plan/tool/terminal 是否提供),DeepChat 只能呈现 agent 给的结构。
- prompt cache 能力按 provider 区分(
promptCacheCapabilities.ts、promptCacheStrategy.ts),不是所有 provider 都能享受缓存折扣。