本节补充第四章中后端相关的实现细节,包括多模型适配、RAG 管线、生成 Pipeline、以及 JSON 容错处理。
4.1 多模型提供商的统一接入
Agent 开发中,你几乎不可能只用一个模型。不同模型擅长不同的事,而且模型厂商的定价、速率限制、区域可用性都在变。设计一个可插拔的多模型架构很有必要。
Provider 注册表模式
用 Map 做一个模型到 Provider 类的映射:
typescript
// ai.service.ts
private providers = new Map<string, new (config: AIProviderConfig) => AIProvider>([
['gpt-4o', OpenAIProvider],
['claude-sonnet-4-5-20250929', ClaudeProvider],
['doubao-seed-2-0-pro', DoubaoProvider],
['gemini-2.0-flash', GeminiProvider],
]);
// 根据模型名获取 Provider 实例
getProvider(modelId: string): AIProvider {
const ProviderClass = this.providers.get(modelId);
if (!ProviderClass) throw new Error(`Unsupported model: ${modelId}`);
const config = this.getConfigForModel(modelId);
return new ProviderClass(config);
}
动态 API Key 路由
不同前缀的模型走不同的 API Key 和 endpoint:
typescript
private getConfigForModel(modelId: string): AIProviderConfig {
if (modelId.startsWith('gpt')) return { apiKey: OPENAI_KEY, baseUrl: 'https://api.openai.com/v1' };
if (modelId.startsWith('claude')) return { apiKey: CLAUDE_KEY, baseUrl: 'https://api.anthropic.com' };
if (modelId.startsWith('doubao')) return { apiKey: DOUBAO_KEY, baseUrl: 'https://ark.cn-beijing.volces.com/api/v3' };
if (modelId.startsWith('gemini')) return { apiKey: GEMINI_KEY, baseUrl: 'https://generativelanguage.googleapis.com' };
}
经验教训: 国内调用 OpenAI 需要走代理,
baseUrl支持自定义很重要。Doubao 的 API 和 OpenAI 兼容,可以复用 OpenAI 的 SDK,但要注意 endpoint 不同。
4.2 生成 Pipeline:分步执行的设计
一个复杂的 Agent 任务(比如根据需求文档生成测试用例),不适合一次性扔给模型。更好的做法是拆成多个步骤,形成 Pipeline:
markdown
需求文档
↓
[Step 1] RAG 上下文构建
- 搜索相似需求
- 搜索历史用例
- 加载知识库
↓
[Step 2] 场景分解
- 把大需求拆成 5-10 个测试场景
- 每个场景有名称 + 关键测试点
↓
[Step 3] 批量生成
- 每个场景独立调用模型
- 并发度控制(建议 2 并发)
- 实时进度推送(WebSocket)
↓
[Step 4] 结果合并 & 落库
Step 2 场景分解的关键代码
typescript
async function decomposeScenarios(context: GenerationContext) {
const prompt = buildDecompositionPrompt(context);
// 调用模型拆分场景,至少返回 5 个
let scenarios = await aiService.decomposeScenarios(prompt);
// 不够 3 个就重试一次
if (scenarios.length < 3) {
scenarios = await aiService.decomposeScenarios(prompt);
}
// 实在不行,降级为单场景
if (scenarios.length === 0) {
scenarios = [{ name: '全功能测试', testPoints: ['覆盖所有功能点'] }];
}
return scenarios;
}
Step 3 批量生成的并发控制
typescript
const CONCURRENCY = 2; // 同时最多 2 个模型调用
async function batchGenerate(scenarios: TestScenario[], context: GenerationContext) {
const results = [];
const queue = [...scenarios];
// 简单的并发池
async function worker() {
while (queue.length > 0) {
const scenario = queue.shift();
const result = await generateForScenario(scenario, context);
results.push(result);
// 推送进度
emitProgress(results.length / scenarios.length * 100);
}
}
// 启动 N 个 worker
await Promise.all(
Array.from({ length: CONCURRENCY }, () => worker())
);
return results;
}
为什么不全部并发? 大模型 API 有速率限制,并发太高会被 429 限流。2-3 并发是安全的甜点。
4.3 JSON 输出的防御性解析
这是 Agent 开发中最容易被低估的问题。大模型输出的 JSON 经常有各种格式问题:
常见的 JSON 出错类型
| 问题 | 示例 | 出现频率 |
|---|---|---|
| 被 Markdown 包裹 | json\n[...]\n |
非常高 |
| 中文引号 | "标题" 而不是 "标题" |
高(中文 prompt) |
| 尾随逗号 | [{...}, {...},] |
中等 |
| 截断(token 用完) | [{...}, { |
偶发但致命 |
| 转义错误 | 字符串内未转义的 " |
中等 |
多层容错策略
typescript
function parseModelOutput(raw: string): any[] {
// 第 1 层:去掉 Markdown 代码块
let cleaned = raw.replace(/```(?:json)?\n?/g, '').replace(/```/g, '').trim();
// 第 2 层:提取 JSON 数组
const bracketMatch = cleaned.match(/\[[\s\S]*\]/);
if (bracketMatch) cleaned = bracketMatch[0];
// 第 3 层:修复中文引号
cleaned = cleaned
.replace(/\u201c/g, '"') // "
.replace(/\u201d/g, '"'); // "
// 第 4 层:移除尾随逗号
cleaned = cleaned.replace(/,(\s*[}\]])/g, '$1');
// 第 5 层:尝试解析
try {
return JSON.parse(cleaned);
} catch (e) {
// 第 6 层:逐对象恢复(处理截断)
return recoverByObjects(cleaned);
}
}
// 处理截断的 JSON ------ 尽量恢复已完成的对象
function recoverByObjects(text: string): any[] {
const results = [];
const objectPattern = /\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g;
let match;
while ((match = objectPattern.exec(text)) !== null) {
try {
results.push(JSON.parse(match[0]));
} catch {
// 跳过无法解析的片段
}
}
return results;
}
真实数据: 在我们的生产环境中,大约 15% 的模型输出需要经过第 2 层以上的修复。用了 Tool Use(第三章 3.4 节)之后,这个比例降到了不到 2%。
4.4 两阶段深度分析(进阶)
对于复杂需求,单次模型调用可能分析得不够深入。一个有效的策略是两阶段调用:
markdown
阶段 1:深度思考模型(慢但深)
- 用 Seed 2.0 Pro(思考模型)做需求深度分析
- 输出自然语言的分析报告
↓
阶段 2:快速执行模型(快但准)
- 把阶段 1 的分析报告 + 原始需求一起给执行模型
- 用 Seed 1.5 Pro(快速模型)做场景分解
typescript
async function twoStageDecompose(context: GenerationContext) {
// 阶段 1:深度分析(思考模型)
const analysisPrompt = '你是资深测试架构师,请对需求进行深度分析...';
const analysis = await callThinkingModel(analysisPrompt, context);
// 阶段 2:把分析结果注入上下文
const enrichedContext = {
...context,
requirements: [
...context.requirements,
`\n---\n## 专家深度分析结果\n\n${analysis}`
]
};
// 用快速模型做场景分解
return await decomposeScenarios(enrichedContext);
}
什么时候用两阶段? 需求文档超过 2000 字、涉及多个功能模块、或业务逻辑复杂时。简单需求直接一阶段就够了。
4.5 模型路由策略
不同的步骤适合不同的模型:
typescript
// model-router.ts
function getModelForStep(step: string, userSelectedModel: string): string {
// 思考类模型(Seed 2.0)用于分析,但生成用快速模型
if (isSeed2Model(userSelectedModel)) {
switch (step) {
case 'analysis': return userSelectedModel; // 深度分析用思考模型
case 'decompose': return FAST_GENERATION_MODEL; // 场景分解用快速模型
case 'generate': return FAST_GENERATION_MODEL; // 用例生成用快速模型
}
}
// 其他模型全程使用同一个
return userSelectedModel;
}
为什么要路由? 思考模型(如 Seed 2.0、o1)很擅长分析但生成速度慢、成本高。把"想"和"做"分开,既保证质量又控制成本。
本节小结
| 实践 | 核心收获 |
|---|---|
| 多模型架构 | Map 注册表 + 动态 Key 路由,加新模型只需一行 |
| Pipeline 模式 | 拆步骤 > 一次性调用,每步可独立优化和重试 |
| JSON 容错 | 至少 5 层防御,Tool Use 能大幅减少出错率 |
| 两阶段分析 | "想"和"做"分开,深度模型分析 + 快速模型执行 |
| 模型路由 | 不同步骤用不同模型,平衡质量和成本 |