1 line
17 KiB
JSON
1 line
17 KiB
JSON
{"repo": "cfool/try-agent", "files": {"src/model/client.ts": {"98fcf3fd123acffb549349a97baa8a1ced09810e": {"t": 1785768203.0420024, "n": 3499, "h": "6c748ce187825f9e", "c": "import { DeepSeekProvider } from \"./providers/deepseek.js\";\nimport { GeminiProvider } from \"./providers/gemini.js\";\nimport { TencentProvider } from \"./providers/tencent.js\";\nimport type { Message, ModelProvider, SendMessageOptions, SendMessageResult, StreamChunk } from \"./providers/types.js\";\nimport { ZhiPuProvider } from \"./providers/zhipu.js\";\n\nexport interface ModelConfig {\n name: string;\n alias?: string;\n provider: ModelProvider;\n}\n\nexport class ModelClient {\n private providers = new Map<string, ModelConfig>();\n private currentModel: string | null = null;\n\n registerModel(model: ModelConfig): void {\n this.providers.set(model.name, model);\n if (!this.currentModel) {\n this.currentModel = model.name;\n }\n }\n\n use(model: string): void {\n if (!this.providers.has(model)) {\n throw new Error(\n `Unknown model \"${model}\". Available: ${[...this.providers.keys()].join(\", \")}`\n );\n }\n this.currentModel = model;\n }\n\n getActiveModel(): ModelConfig {\n if (!this.currentModel || !this.providers.has(this.currentModel)) {\n throw new Error(\"No model registered.\");\n }\n return this.providers.get(this.currentModel)!;\n }\n\n /** Return all registered models with their active status. */\n listModels(): Array<{ name: string; alias?: string; active: boolean }> {\n return [...this.providers.values()].map((m) => ({\n name: m.name,\n alias: m.alias,\n active: m.name === this.currentModel,\n }));\n }\n\n async sendMessage(messages: Message[], options?: SendMessageOptions): Promise<SendMessageResult> {\n const modelName = options?.model ?? this.currentModel;\n if (!modelName || !this.providers.has(modelName)) {\n throw new Error(\n `Model \"${modelName}\" not found. Available: ${[...this.providers.keys()].join(\", \")}`\n );\n }\n const provider = this.providers.get(modelName)!.provider;\n return provider.sendMessage(messages, options);\n }\n\n async *streamMessage(\n messages: Message[],\n options?: SendMessageOptions\n ): AsyncGenerator<StreamChunk, void, unknown> {\n const modelName = options?.model ?? this.currentModel;\n if (!modelName || !this.providers.has(modelName)) {\n throw new Error(\n `Model \"${modelName}\" not found. Available: ${[...this.providers.keys()].join(\", \")}`\n );\n }\n const provider = this.providers.get(modelName)!.provider;\n yield* provider.streamMessage(messages, options);\n }\n}\n\nconst client = new ModelClient();\n\n// Register available providers based on env vars\nconst geminiKey = process.env.GEMINI_API_KEY;\nif (geminiKey) {\n client.registerModel({\n name: 'gemini-3-flash-preview',\n alias: 'Gemini-3-Flash',\n provider: new GeminiProvider({ apiKey: geminiKey, model: 'gemini-3-flash-preview' }),\n });\n}\n\nconst deepseekKey = process.env.DEEPSEEK_API_KEY;\nif (deepseekKey) {\n client.registerModel({\n name: 'deepseek-v3.2',\n alias: 'DeepSeek-V3.2',\n provider: new DeepSeekProvider({ apiKey: deepseekKey, model: 'deepseek-chat' })\n });\n}\n\nconst zhipuKey = process.env.ZHIPU_API_KEY;\nif (zhipuKey) {\n client.registerModel({\n name: 'glm-5',\n alias: 'GLM-5',\n provider: new ZhiPuProvider({ apiKey: zhipuKey, model: 'glm-5' })\n });\n}\n\nconst tencentKey = process.env.TENCENT_API_KEY;\nif (tencentKey) {\n client.registerModel({\n name: 'hunyuan-turbos-latest',\n alias: 'Hunyuan-Turbos',\n provider: new TencentProvider({ apiKey: tencentKey, model: 'hunyuan-turbos-latest' })\n });\n}\n\nexport { client };\n"}}, "docs/01b-multi-model.md": {"98fcf3fd123acffb549349a97baa8a1ced09810e": {"t": 1785768879.2355077, "n": 5853, "h": "ad43ebb958ce8639", "c": "---\ntitle: 多模型支持\nnav_order: 2\nparent: 实战开发\n---\n\n# 第 1.5 阶段:给 AI 换个\"大脑\" (Multi-Model Support)\n\n**Branch:** `01b-multi-model`\n\n在[上一章](./01-hello-ai.md)我们给 AI 换了各种\"人设\"(System Prompt),但无论人设怎么变,驱动它的\"大脑\"始终是同一个模型。\n\n如果说 System Prompt 是演员的**剧本**,那模型供应商 (Model Provider) 就是演员的**大脑**。同一个剧本,让不同的演员来演,效果天差地别。\n\n这个项目就像一个\"大脑\"插拔机器人,我们内置了对 **Gemini**、**DeepSeek**、**智谱 (ZhiPu)** 和 **腾讯混元 (Tencent Hunyuan)** 的支持。你可以随时给你的 AI 代理换个\"脑子\"!\n\n## 为什么要换\"大脑\"?\n\n不同的\"大脑\"有不同的特长:\n\n* **Gemini (gemini-3-flash-preview)**: Google 出品,综合能力强,像个全能学霸。\n* **DeepSeek (deepseek-v3.2)**: 在代码和推理方面有奇效,是个逻辑鬼才。\n* **智谱 (glm-5)**: 中文理解能力超群,是个本土文学家。\n* **腾讯混元 (hunyuan-turbos-latest)**: 腾讯自研大模型,国内访问快、中文能力强。\n* **成本与速度**: 不同模型的定价和响应速度也不同。\n\n换着用,总能找到最适合你当前任务的那个\"大脑\"。\n\n## 核心问题:各家 API 协议不统一\n\n要支持多模型,第一个绕不开的问题就是——**各家供应商的 API 协议各不相同**。\n\n举个例子,同样是\"发送一条消息并调用工具\",Gemini 和 OpenAI 系的请求格式天差地别:\n\n| 差异点 | Gemini 原生 API | OpenAI 兼容 API (DeepSeek / 智谱 / 混元) |\n|---|---|---|\n| 端点格式 | `models/{model}:generateContent` | `/chat/completions` |\n| 角色名称 | `user` / `model` | `user` / `assistant` / `system` |\n| System Prompt | 顶层 `system_instruction` 字段 | `role: \"system\"` 的消息 |\n| 工具声明 | `tools: [{ function_declarations: [...] }]` | `tools: [{ type: \"function\", function: {...} }]` |\n| 工具调用结果 | `role: \"user\"` + `functionResponse` 部分 | `role: \"tool\"` + `tool_call_id` |\n| 工具调用参数 | 直接是 JSON 对象 (`args: {...}`) | 序列化为字符串 (`arguments: \"{...}\"`) |\n| 认证方式 | URL 参数 `?key=xxx` | Header `Authorization: Bearer xxx` |\n\n> 关于工具调用的协议差异,在03/04,给agent增加工具调用的章节中会有相关代码\n\n如果你要做一个支持多模型供应商的 Agent,就必须考虑这些协议差异,并做好协议转换。好消息是,目前大部分国内模型供应商(DeepSeek、智谱、腾讯混元等)都提供了兼容 OpenAI 格式的 API,这大大降低了适配成本——你只需要处理两种协议:**Gemini 原生** 和 **OpenAI 兼容**。\n\n## 架构设计:Provider 抽象层\n\n理解了协议差异问题后,我们的解决方案就很自然了:**统一接口 + 分层抽象**。\n\n```\nModelClient (统一入口:注册、切换、路由)\n │\n ├── GeminiProvider (Gemini 原生协议适配)\n │ 转换: Message ↔ GeminiContent\n │ 角色映射: \"model\" → \"model\", \"tool\" → \"user\" + functionResponse\n │\n └── OpenAICompatibleProvider (OpenAI 协议适配)\n 转换: Message ↔ ChatMessage\n 角色映射: \"model\" → \"assistant\", \"tool\" → \"tool\" + tool_call_id\n │\n ├── DeepSeekProvider (baseUrl: api.deepseek.com)\n ├── ZhiPuProvider (baseUrl: open.bigmodel.cn/api/paas/v4)\n └── TencentProvider (baseUrl: api.hunyuan.cloud.tencent.com/v1)\n```\n\n### 第一层:统一的 ModelProvider 接口\n\n不管底层协议怎么变,上层代码只看一个接口:\n\n```typescript\n// src/model/providers/types.ts\nexport interface ModelProvider {\n name: string;\n sendMessage(\n messages: Message[],\n options?: SendMessageOptions\n ): Promise<SendMessageResult>;\n}\n```\n\n`SendMessageOptions` 支持传入 `systemInstruction` 和 `tools`(函数声明),`SendMessageResult` 会返回文本或函数调用请求。上层的 Chat、Agent Loop、工具调用等逻辑完全不需要关心底层用的是哪家 API。\n\n### 第二层:协议适配\n\n每个 Provider 的核心工作就是**协议转换**——把统一的 `Message` 翻译成各家 API 能理解的格式,再把响应翻译回来。\n\n**GeminiProvider** 需要处理 Gemini 特有的格式(比如工具结果要以 `role: \"user\"` 发送,函数参数直接是 JSON 对象),以及 Gemini 独有的 `thoughtSignature` 字段。\n\n**OpenAICompatibleProvider** 则处理 OpenAI 格式的转换(`role: \"model\"` → `\"assistant\"`、函数参数需要 `JSON.stringify` 序列化等)。由于 DeepSeek、智谱、腾讯混元都兼容这套格式,它们只需继承这个基类,提供自己的 `baseUrl` 就行:\n\n```typescript\n// src/model/providers/deepseek.ts — 只需 5 行就完成一个新供应商的接入\nexport class DeepSeekProvider extends OpenAICompatibleProvider {\n constructor(options: { apiKey: string; model: string }) {\n super({\n name: \"deepseek\",\n apiKey: options.apiKey,\n model: options.model,\n baseUrl: \"https://api.deepseek.com\",\n });\n }\n}\n```\n\n这意味着,如果你将来要接入一个新的 OpenAI 兼容供应商(比如 Moonshot、百川等),只需要写一个类似的子类,改个 `baseUrl` 就完事了。\n\n### 第三层:ModelClient 注册与切换\n\n`ModelClient` 负责模型的注册和运行时切换。每个模型以 `name`(模型标识)和 `alias`(显示名)注册:\n\n```typescript\n// src/model/client.ts\nclient.registerModel({\n name: 'deepseek-v3.2',\n alias: 'DeepSeek-V3.2',\n provider: new DeepSeekProvider({ apiKey: deepseekKey, model: 'deepseek-chat' })\n});\n```\n\n`name` 是你在 `/use` 命令和 `MODEL` 环境变量中使用的标识符,`alias` 是展示给用户看的友好名称。\n\n## 如何换\"大脑\"?\n\n三步走:搞到 Key -> 配置 Key -> 切换着用。\n\n### 1. 搞到 API Keys\n\n就像你需要一把钥匙才能打开 Gemini 的门一样,你也需要其他家的钥匙。\n\n* **Gemini**: 去 [Google AI Studio](https://aistudio.google.com/) 获取 API Key。\n* **DeepSeek**: 去 [DeepSeek 官网](https://platform.deepseek.com/api_keys) 注册并获取 API Key。\n* **智谱**: 去 [智谱开放平台](https://open.bigmodel.cn/overview) 注册并获取 API Key。\n* **腾讯混元**: 去 [腾讯混元控制台](https://hunyuan.cloud.tencent.com/#/app/apiKeyMana) 获取 API Key。\n\n### 2. 配置 `.env` 文件\n\n把你新搞到的钥匙串在钥匙链 (`.env` 文件) 上:\n\n```env\n# Google Gemini (全能学霸)\nGEMINI_API_KEY=AIzaSyDxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\n# DeepSeek (逻辑鬼才)\nDEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx\n\n# 智谱 (本土文学家)\nZHIPU_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxx\n\n# 腾讯混元 (国产之光)\nTENCENT_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx\n```\n\n程序启动时会自动检测哪些 Key 存在,并注册对应的模型。**不需要的供应商可以不配置**,只要至少配了一个就行。\n\n### 3. 切换\"大脑\"\n\n我们提供了两种切换方式:\n\n#### 方式一:指定默认模型\n\n想让你的 AI 默认就用某个模型?在 `.env` 里用 `MODEL` 环境变量指定模型名:\n\n```env\n# 指定默认模型(填模型注册名,不是 provider 名)\nMODEL=hunyuan-turbos-latest\n```\n\n支持的模型名:\n\n| 模型名 | 别名 | 供应商 |\n|---|---|---|\n| `gemini-3-flash-preview` | Gemini-3-Flash | Google |\n| `deepseek-v3.2` | DeepSeek-V3.2 | DeepSeek |\n| `glm-5` | GLM-5 | 智谱 |\n| `hunyuan-turbos-latest` | Hunyuan-Turbos | 腾讯 |\n\n如果不设置 `MODEL`,程序会使用第一个成功注册的模型。\n\n#### 方式二:运行时热插拔\n\n更酷的是,你可以在聊天过程中,随时给 AI 换\"脑\"!就像给机器人换芯片一样。\n\n只要在聊天框里输入 `/use` 命令 + **模型名**:\n\n```\nYou: /use deepseek-v3.2\n```\n> 系统会回应:\n> `Switched to Model: DeepSeek-V3.2`\n\n然后,你的下一句话就会由 DeepSeek 来处理了。\n\n```\nYou: /use glm-5\n```\n> 系统会回应:\n> `Switched to Model: GLM-5`\n\n切换到腾讯混元:\n\n```\nYou: /use hunyuan-turbos-latest\n```\n\n想换回 Gemini?也一样:\n\n```\nYou: /use gemini-3-flash-preview\n```\n\n> **注意**:`/use` 后面跟的是模型注册名(即上表中\"模型名\"列的值),不是供应商名称。如果输入了不存在的名称,系统会提示可用的模型列表。\n\n> **提示**:在 [第 9 部分:TUI](./09-tui.md) 中,`/use` 命令将升级为交互式的 `/model` 命令,通过选择面板切换模型,无需手动输入模型名。\n\n**动手试试:**\n1. 配置好至少两个模型的 API Key。\n2. 启动项目 (`npm start`)。\n3. 问一个相同的问题,比如\"用 Python 写一个快速排序\",然后用 `/use` 命令切换模型,看看不同\"大脑\"给出的代码风格和解释有什么不一样。\n\n**能力:** 可以在多个\"大脑\"之间无缝切换,为不同的任务选择最合适的 AI 模型。\n**类比:** 你现在拥有一个复仇者联盟,而不是只有一个钢铁侠。\n"}}, "docs/SETUP.md": {"98fcf3fd123acffb549349a97baa8a1ced09810e": {"t": 1785768891.702059, "n": 2456, "h": "567d5cac9e693d68", "c": "---\ntitle: 环境配置\nnav_order: 1\nparent: 准备工作\n---\n\n# 环境配置 (Setup)\n\n在开始构建 Agent 之前,我们需要准备好开发环境。\n\n## 1. 前置要求\n\n确保你的电脑上安装了:\n\n* **Node.js**: 推荐 v18 或更高版本。\n* **Git**: 用于版本控制。\n* **包管理器**: npm (随 Node.js 安装), pnpm, 或 yarn。\n\n## 2. 获取 Gemini API Key\n\n本项目使用 Google Gemini 模型。你需要一个 API Key:\n\n1. 访问 [Google AI Studio](https://aistudio.google.com/)。\n2. 登录你的 Google 账号。\n3. 点击 **\"Get API key\"**。\n4. 点击 **\"Create API key\"**。\n5. 复制生成的 Key(以 `AIza` 开头)。\n\n> **注意**:Gemini API 目前有免费额度,足够开发测试使用。\n\n## 3. 安装项目依赖\n\n在终端中执行:\n\n```bash\n# 1. 克隆项目 (如果你还没克隆)\n# git clone <repository-url>\n# cd try_agent\n\n# 2. 安装依赖\nnpm install\n```\n\n## 4. 配置环境变量\n\n项目根目录下需要一个 `.env` 文件来存储敏感配置(如 API Key)。\n\n1. 新建一个配置文件:\n ```bash\n touch .env\n ```\n\n2. 编辑 `.env` 文件,填入你的 API Key:\n ```env\n GEMINI_API_KEY=AIzaSyDxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n ```\n\n## 5. 配置多模型供应商 (可选)\n\n除了 Google Gemini,本项目还支持 DeepSeek、智谱 (ZhiPu) 和腾讯混元 (Tencent Hunyuan)。\n\n1. **获取 API Key**:\n * **DeepSeek**: 访问 [DeepSeek 官网](https://platform.deepseek.com/api_keys) 获取 API Key。\n * **智谱**: 访问 [智谱开放平台](https://open.bigmodel.cn/overview) 获取 API Key。\n * **腾讯混元**: 访问 [腾讯混元控制台](https://cloud.tencent.com/product/hunyuan) 获取 API Key。\n\n2. **配置环境变量**:\n 在 `.env` 文件中,添加相应模型的 API Key(不需要的供应商可以不配):\n ```env\n # ... 其他配置\n DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx\n ZHIPU_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxx\n TENCENT_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx\n ```\n\n3. **指定默认模型 (可选)**:\n 你可以通过 `MODEL` 环境变量设置启动时默认使用的模型。值为模型注册名:\n ```env\n # ... 其他配置\n MODEL=hunyuan-turbos-latest\n ```\n 可选值:`gemini-3-flash-preview`、`deepseek-v3.2`、`glm-5`、`hunyuan-turbos-latest`。\n 如果不设置,程序将使用第一个成功注册的模型。\n\n## 6. 运行与切换模型\n\n一切准备就绪,运行项目:\n\n```bash\nnpm start\n```\n\n如果看到类似下面的输出,说明配置成功!\n\n```text\n[DEBUG] Using preferred model: hunyuan-turbos-latest\nCurrent Model: Hunyuan-Turbos\nAI Chat (type '/exit' to quit, '/new' to start a new chat, '/use <model>' to switch, '/agents' to list agents, '/skills' to list skills)\n\nYou:\n```\n\n### 在运行时切换模型\n\n你可以在聊天中随时使用 `/use` 命令切换模型,参数为模型注册名。\n\n* 切换到 DeepSeek:\n ```\n You: /use deepseek-v3.2\n ```\n* 切换到智谱:\n ```\n You: /use glm-5\n ```\n* 切换到腾讯混元:\n ```\n You: /use hunyuan-turbos-latest\n ```\n* 切换回 Gemini:\n ```\n You: /use gemini-3-flash-preview\n ```\n\n> **提示**:在 [第 9 部分:TUI](./09-tui.md) 中,`/use` 命令将升级为交互式的 `/model` 命令,通过选择面板切换模型,无需手动输入模型名。\n\n## 下一步\n\n环境配置完成后,请阅读 [**第 0 章:核心概念**](./00-concepts.md),了解 Agent 背后的基本原理。\n"}}}} |