跳到主要内容

两层 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.mdprocessStream 干的。基类注释把这点写死了: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.tsgithubCopilotProvider.tsvoiceAIProvider.tsacpProvider.ts。AI SDK 那条还单独有一层 aiSdk/(messageMapperproviderFactorystreamAdaptertoolMapper……)把各家 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 侧 sessionacpSessionManager.tsacpSessionPersistence.ts
配置状态session 级配置选项acpConfigState.ts
内容映射ACP 消息 ⇄ DeepChat 块acpContentMapper.tsacpMessageFormatter.ts
文件/终端agent 的 fs / terminal 能力acpFsHandler.tsacpTerminalManager.ts
MCP 配置转换把 DeepChat 的 MCP 配置喂给 agentmcpConfigConverter.tsmcpTransportFilter.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:367-380)——所以 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.tspromptCacheStrategy.ts),不是所有 provider 都能享受缓存折扣。

7. 代码地图

主题文件路径符号名
Provider 总机src/main/presenter/llmProviderPresenter/index.tsLLMProviderPresentergetProviderInstanceprepareAcpSession
抽象基类 / 契约src/main/presenter/llmProviderPresenter/baseProvider.tsBaseLLMProvidercoreStreamcompletions
AI SDK providersrc/main/presenter/llmProviderPresenter/providers/aiSdkProvider.tsaiSdkProvider
AI SDK 适配层src/main/presenter/llmProviderPresenter/aiSdk/messageMapperstreamAdaptertoolMapper
ACP providersrc/main/presenter/llmProviderPresenter/providers/acpProvider.tsacpProvider
ACP runtime helpersrc/main/presenter/llmProviderPresenter/acp/acpProcessManageracpSessionManagermcpConfigConverter
prompt cachesrc/main/presenter/llmProviderPresenter/promptCacheStrategy.tspromptCacheStrategy