独立产品 AI 代码仓库管理:多项目共享 AI 能力的架构设计

发布时间:2026/7/19 17:31:49
独立产品 AI 代码仓库管理:多项目共享 AI 能力的架构设计 独立产品 AI 代码仓库管理多项目共享 AI 能力的架构设计一、AI 能力碎片化的管理困境每个项目都在重复造轮子独立开发者常常同时维护多个产品——一个 Web 端管理后台、一个小程序端用户端、一个 API 服务、以及若干个工具脚本。当 AI 能力如 LLM 调用、文本嵌入、向量检索、Prompt 管理需要在多个项目间共享时最常见的反模式是复制粘贴 局部修改把上一个项目的 AI 调用代码复制到新项目修改 API Key 和提示词然后逐渐形成多个版本的发散演化的代码。这种碎片化带来的问题在 34 个项目时开始显现某个 Prompt 模板优化了需要手动同步到 3 个项目的代码API 调用增加了重试和超时逻辑但只有 2 个项目更新了文本嵌入模型升级了新版本需要检查 4 个项目的兼容性。这些维护工作耗费的时间很快超过了当初独立开发、快速上线节省的时间。AI 代码仓库管理的核心思想是将 AI 能力作为独立的共享模块进行治理使其在多项目之间保持单一数据源Single Source of Truth。这不仅仅是一个库的封装更是一种架构纪律——确保 AI 能力的使用方式、Prompt 模板、模型配置和错误处理策略在组织内保持一致。为了实现这一目标架构设计通常分为三个核心层级。最上层是业务项目层包含 Web 管理端、小程序用户端、API 服务及工具脚本等具体应用场景。中间层是 AI 共享能力层采用 Monorepo 模式管理核心组件包括版本化 Prompt 模板库、统一封装的 LLM 客户端、负责向量化与检索的 Embedding 服务以及编排工具调用的 Agent 运行时。最底层是基础设施层提供 LLM API如 OpenAI/Claude、向量数据库如 Qdrant/pgvector以及配置中心用于模型选择与密钥管理。业务项目通过依赖中间层的共享能力模块间接调用底层基础设施从而确保各业务端在 AI 能力使用上的一致性。这种分层结构清晰地界定了职责边界业务层专注于具体场景逻辑共享层负责 AI 能力的标准化封装基础设施层则提供稳定的底层服务支持。接下来我们将重点探讨中间层的设计细节。二、AI 能力共享层的分层设计2.1 Prompt 模板库版本化与可测试Prompt 是 AI 应用中变化最频繁的部分。一个优化得当的 Prompt 能够将 LLM 的响应质量提升 50% 以上但前提是它的修改记录可追溯、效果可评估、变更不影响线上服务。Prompt 模板库的设计遵循以下原则模板文件与代码分离Prompt 模板以单独的.md或.yaml文件存储与业务逻辑解耦。修改 Prompt 不需要重新部署代码。语义化版本每个 Prompt 模板标注版本号v1.2.0支持 A/B 测试时同时使用多个版本。自动化回归测试为 Prompt 编写测试用例——输入一组已知的用户消息验证输出是否包含预期的关键信息。模板变量注入支持{{variable_name}}形式的占位符运行时注入上下文数据。2.2 LLM 客户端统一调用封装在多个项目中直接调用 OpenAI/Claude API 的危险在于重试策略不一致有些 3 次重试、有些没有、超时配置不一致有些 30s、有些无限期、错误处理不一致有些静默忽略、有些打印到控制台。统一的 LLM 客户端负责重试与超时统一的指数退避重试最多 3 次、每次间隔 1s/2s/4s和超时配置默认 60s。降级策略当主模型不可用时如 OpenAI 服务故障自动降级到备用模型如 Claude。请求日志与监控记录每次调用的延迟、Token 消耗和响应状态用于成本核算和性能优化。流式响应封装统一处理 SSE 流式响应各业务端使用相同的回调接口。2.3 Embedding 服务向量化与检索的抽象文本嵌入是推荐、搜索和 RAG 的关键基础设施。Embedding 服务的核心能力包括统一 Embedding 接口提供embed(texts: string[]) → number[][]的标准接口屏蔽底层模型差异。向量数据库适配封装 Qdrant、pgvector、Milvus 等向量数据库的差异通过统一的search/add/delete接口操作。批量处理优化对大批量文本的嵌入生成进行批处理和速率控制避免触发 API 限流。三、Monorepo 结构的生产级实现以下实现展示了基于 Monorepo 的 AI 共享能力仓库的组织方式和核心模块代码packages/ ├── ai-core/ # LLM 客户端核心封装 │ ├── src/ │ │ ├── client.ts # LLM 调用客户端 │ │ ├── types.ts # 类型定义 │ │ ├── retry.ts # 重试策略 │ │ └── streaming.ts # 流式响应处理 │ └── package.json ├── ai-prompts/ # Prompt 模板库 │ ├── prompts/ # 按场景分组的 Prompt 文件 │ │ ├── content/ # 内容生成相关 │ │ ├── analysis/ # 分析诊断相关 │ │ └── recommend/ # 推荐排序相关 │ ├── src/ │ │ ├── loader.ts # Prompt 加载器 │ │ └── version.ts # 版本管理 │ └── __tests__/ # Prompt 回归测试 ├── ai-embeddings/ # Embedding 服务 │ ├── src/ │ │ ├── embedder.ts # 嵌入生成器 │ │ └── vector-store.ts # 向量数据库封装 │ └── package.json └── ai-agent/ # Agent 运行时 ├── src/ │ ├── agent.ts # Agent 执行引擎 │ └── tools.ts # 工具注册与调用 └── package.json// packages/ai-core/src/client.ts /** * 统一 LLM 调用客户端 * 职责抽象多个 AI 服务提供商的差异提供统一接口 */ type Provider openai | anthropic | groq; interface LLMConfig { provider: Provider; model: string; apiKey: string; options?: { temperature?: number; maxTokens?: number; timeout?: number; }; } interface LLMRequest { systemPrompt: string; userMessage: string; // 历史消息上下文 history?: Array{ role: user | assistant; content: string }; } interface LLMResponse { content: string; model: string; usage: { promptTokens: number; completionTokens: number; totalTokens: number; }; latency: number; } interface LLMStreamCallbacks { onToken: (token: string) void; onComplete: (response: LLMResponse) void; onError: (error: Error) void; } class LLMClient { private config: LLMConfig; private retryConfig: { maxRetries: number; baseDelay: number; }; constructor(config: LLMConfig) { this.config config; this.retryConfig { maxRetries: 3, baseDelay: 1000, // 基础延迟 1s }; } /** * 同步调用 LLM一次性请求-响应 */ async call(request: LLMRequest): PromiseLLMResponse { const startTime Date.now(); return this.withRetry(async () { // 根据 provider 路由到对应的 API switch (this.config.provider) { case openai: return this.callOpenAI(request); case anthropic: return this.callAnthropic(request); default: throw new Error(不支持的 Provider: ${this.config.provider}); } }, startTime); } /** * 流式调用 LLM逐 Token 回调 */ async stream(request: LLMRequest, callbacks: LLMStreamCallbacks): Promisevoid { try { const response await this.createStreamRequest(request); const reader response.getReader(); const decoder new TextDecoder(); let fullContent ; const startTime Date.now(); while (true) { const { done, value } await reader.read(); if (done) break; const chunk decoder.decode(value, { stream: true }); const lines chunk.split(\n).filter((line) line.trim() ! ); for (const line of lines) { if (line.startsWith(data: )) { const data line.slice(6); if (data [DONE]) break; try { const parsed JSON.parse(data); const token parsed.choices?.[0]?.delta?.content ?? ; if (token) { fullContent token; callbacks.onToken(token); } } catch { // 忽略解析失败的 chunk } } } } callbacks.onComplete({ content: fullContent, model: this.config.model, usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }, latency: Date.now() - startTime, }); } catch (error) { callbacks.onError(error instanceof Error ? error : new Error(String(error))); } } /** * 指数退避重试 */ private async withRetry( fn: () PromiseLLMResponse, startTime: number ): PromiseLLMResponse { let lastError: Error | null null; for (let attempt 0; attempt this.retryConfig.maxRetries; attempt) { try { const response await fn(); response.latency Date.now() - startTime; return response; } catch (error) { lastError error instanceof Error ? error : new Error(String(error)); // 判断是否为可重试的错误网络超时、服务过载 if (!this.isRetryableError(lastError)) { throw lastError; } if (attempt this.retryConfig.maxRetries) { // 指数退避: 1s, 2s, 4s const delay this.retryConfig.baseDelay * Math.pow(2, attempt); console.warn( [LLM] 第 ${attempt 1} 次尝试失败${delay}ms 后重试: ${lastError.message} ); await this.sleep(delay); } } } throw lastError; } /** * 判断错误是否可重试 */ private isRetryableError(error: Error): boolean { const retryableMessages [ timeout, rate limit, server error, service unavailable, ECONNRESET, ETIMEDOUT, ]; return retryableMessages.some((msg) error.message.toLowerCase().includes(msg) ); } /** * 调用 OpenAI API */ private async callOpenAI(request: LLMRequest): PromiseLLMResponse { const messages [ { role: system as const, content: request.systemPrompt }, ...(request.history ?? []).map((h) ({ role: h.role as user | assistant, content: h.content, })), { role: user as const, content: request.userMessage }, ]; const response await fetch(https://api.openai.com/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${this.config.apiKey}, }, signal: AbortSignal.timeout(this.config.options?.timeout ?? 60_000), body: JSON.stringify({ model: this.config.model, messages, temperature: this.config.options?.temperature ?? 0.7, max_tokens: this.config.options?.maxTokens ?? 4096, }), }); if (!response.ok) { const errorBody await response.text(); throw new Error(OpenAI API 错误 (${response.status}): ${errorBody}); } const data await response.json(); const choice data.choices[0]; return { content: choice.message.content, model: data.model, usage: { promptTokens: data.usage.prompt_tokens, completionTokens: data.usage.completion_tokens, totalTokens: data.usage.total_tokens, }, latency: 0, // 由 withRetry 补充 }; } /** * 调用 Anthropic API */ private async callAnthropic(request: LLMRequest): PromiseLLMResponse { const messages [ { role: user as const, content: request.userMessage }, ]; const response await fetch(https://api.anthropic.com/v1/messages, { method: POST, headers: { Content-Type: application/json, x-api-key: this.config.apiKey, anthropic-version: 2023-06-01, }, signal: AbortSignal.timeout(this.config.options?.timeout ?? 60_000), body: JSON.stringify({ model: this.config.model, system: request.systemPrompt, messages, max_tokens: this.config.options?.maxTokens ?? 4096, }), }); if (!response.ok) { const errorBody await response.text(); throw new Error(Anthropic API 错误 (${response.status}): ${errorBody}); } const data await response.json(); return { content: data.content[0].text, model: data.model, usage: { promptTokens: data.usage.input_tokens, completionTokens: data.usage.output_tokens, totalTokens: data.usage.input_tokens data.usage.output_tokens, }, latency: 0, }; } private async createStreamRequest(_request: LLMRequest): PromiseReadableStreamUint8Array { throw new Error(Not implemented); } private sleep(ms: number): Promisevoid { return new Promise((resolve) setTimeout(resolve, ms)); } } // ---- Prompt 模板加载器 ---- interface PromptTemplate { name: string; version: string; system: string; user: string; // 用户消息模板支持 {{}} 占位符 metadata: { description: string; expectedOutputFormat: string; tags: string[]; }; } class PromptRegistry { private prompts: Mapstring, PromptTemplate new Map(); /** * 从 prompts/ 目录加载所有模板 * 在应用启动时执行一次 */ async loadAll(): Promisevoid { // 实际实现中扫描 prompts/ 目录下的 .yaml 文件 const promptFiles await this.discoverPromptFiles(); for (const file of promptFiles) { const template this.parsePromptFile(file); // 以 nameversion 作为唯一键 const key ${template.name}${template.version}; this.prompts.set(key, template); } console.log([Prompts] 已加载 ${this.prompts.size} 个模板); } /** * 按名称和版本获取 Prompt 模板 * param name 模板名称 * param version 版本号默认取最新 */ get(name: string, version?: string): PromptTemplate | undefined { if (version) { return this.prompts.get(${name}${version}); } // 获取预期版本的模板 const entries Array.from(this.prompts.entries()) .filter(([key]) key.startsWith(${name})) .sort() .reverse(); // 降序version 最大的在前 return entries[0]?.[1]; } /** * 渲染 Prompt将变量注入模板 */ render(template: PromptTemplate, variables: Recordstring, string): LLMRequest { let userMessage template.user; let systemPrompt template.system; // 替换模板中的 {{variable}} 占位符 for (const [key, value] of Object.entries(variables)) { const placeholder new RegExp(\\{\\{\\s*${key}\\s*\\}\\}, g); userMessage userMessage.replace(placeholder, value); systemPrompt systemPrompt.replace(placeholder, value); } return { systemPrompt, userMessage }; } private async discoverPromptFiles(): Promisestring[] { return []; // Stub } private parsePromptFile(_content: string): PromptTemplate { return { name: , version: 1.0.0, system: , user: , metadata: { description: , expectedOutputFormat: , tags: [] }, }; } } export { LLMClient, PromptRegistry }; export type { LLMConfig, LLMRequest, LLMResponse, LLMStreamCallbacks, PromptTemplate };四、共享架构的治理权重与兼容性挑战4.1 版本升级的兼容性管理共享模块的一处修改可能影响所有依赖它的项目。版本管理的核心难题是如何在不破坏已有项目的前提下迭代 AI 能力。建议采用双轨制稳定轨以语义化版本发布向前兼容的改动如增加新参数作为 minor 版本不兼容的改动如修改函数签名作为 major 版本。实验轨使用next或canary标签发布实验性功能供早期采用者试用收集反馈后再并入稳定轨。各个业务项目通过package.json中的版本约束决定自己的升级节奏而不是被迫同步升级。4.2 Prompt 的跨项目一致性 vs 项目定制共享 Prompt 模板库的核心价值是一致性但不同项目如管理端和用户端对同一 AI 能力的输出格式和语气要求可能不同。解决思路是将 Prompt 分为核心指令和项目定制两部分核心指令推理逻辑、输出结构、约束条件在共享 Prompt 中定义项目定制语气风格、输出格式、示例偏好通过变量注入或拼接实现。4.3 Monorepo 的构建与部署成本Monorepo 结构带来代码共享便利的同时也引入了构建和部署的复杂度。每个业务项目打包时需要正确引用 workspace 依赖CI/CD 需要做到按变更范围触发构建——仅当 AI 共享模块变更时才触发所有依赖项目的集成测试。Turborepo 或 Nx 等工具可以显著降低这一复杂度。五、总结独立产品的 AI 代码仓库管理本质是将复制粘贴式开发升级为共享模块式治理。通过 Monorepo 结构将 Prompt 模板库、LLM 客户端、Embedding 服务和 Agent 运行时作为独立包进行管理在多个业务项目间形成单一数据源。Prompt 模板的版本化管理确保了优化可追溯、变更可测试统一 LLM 客户端确保了重试、超时、降级策略的一致性Embedding 服务提供了跨项目的向量化基础设施。落地建议从统一 LLM 客户端开始——封装一个跨项目复用的 LLM 调用层接入重试和日志功能替代各项目中散落的直接 API 调用。这一步的工程成本最低约 12 天开发但能立即消除 AI 调用行为的碎片化。然后逐步将 Prompt 模板和 Embedding 服务迁移到共享包中形成完整的 AI 能力共享层。