本文基于一个真实的 AI 测试用例生成平台的开发经验,帮助"听过 AI 但没写过 Agent"的开发者快速入门。
一、Agent 到底是什么?
1.1 一句话定义
Agent = LLM + 工具 + 自主决策循环
普通的 AI API 调用是"一问一答"——你给一段 prompt,它返回一段文本。而 Agent 是一个能自主规划、调用工具、根据结果继续推理的程序。
打个比方:
- API 调用 像一个只会接电话回答问题的客服
- Agent 像一个能自己查系统、填工单、打电话协调的项目经理
1.2 Agent 的核心能力
用户需求
↓
┌──────────────────┐
│ LLM(大脑) │ ← 理解意图、规划步骤
│ │
│ ┌──────────────┐ │
│ │ 工具调用 │ │ ← 搜索数据库、调用 API、读写文件
│ └──────────────┘ │
│ ┌──────────────┐ │
│ │ 记忆/上下文 │ │ ← 记住历史对话、参考过往经验
│ └──────────────┘ │
│ ┌──────────────┐ │
│ │ 自主循环 │ │ ← 检查结果、决定下一步、自我纠错
│ └──────────────┘ │
└──────────────────┘
↓
输出结果
1.3 2026 年,为什么现在要学?
三个趋势让 Agent 开发从"研究课题"变成了"工程实践":
- 模型能力跃升:Claude 4.5/4.6、GPT-5 系列的推理能力已经足够支撑复杂的多步任务
- 工具生态成熟:MCP(Model Context Protocol)让模型连接外部工具有了统一标准
- 开发工具链完善:Claude Code、Cursor 等 AI-native 开发工具让开发 Agent 本身也变得高效
一个真实案例:我们用 Agent 技术构建了一个测试用例自动生成平台。输入产品需求文档,它会:
- 自动理解需求内容
- 从历史数据库中检索相似需求和测试用例(RAG)
- 拆解为多个测试场景
- 为每个场景批量生成测试用例
- 自动进行质量审查
原来一个测试工程师需要 2-3 天完成的工作,现在 5 分钟就能生成初稿。
1.4 Agent 的典型应用场景
| 场景 | 传统方式 | Agent 方式 |
|---|---|---|
| 测试用例编写 | 人工逐条编写 | 需求文档 → 自动生成 + 人工审查 |
| 代码审查 | 人工逐行检查 | Agent 自动扫描 + 定位问题 + 建议修复 |
| 客服系统 | 关键词匹配 | 理解意图 → 查知识库 → 组织回答 |
| 数据分析 | 手动写 SQL | 自然语言描述 → 生成 SQL → 执行 → 可视化 |
二、技术栈选型:做减法
入门阶段最大的坑是"技术选型焦虑"。这一章帮你做减法:先跑通,再优化。
2.1 LLM API —— 别纠结,先跑通一个
入门推荐:选一个能用的就行。
| 模型 | 优势 | 适合场景 | 接入难度 |
|---|---|---|---|
| Claude 4.5 Sonnet | 推理强、中文好 | 复杂逻辑、代码生成 | 中(需代理或 API Key) |
| GPT-4o | 生态最大、教程最多 | 通用场景 | 低 |
| Doubao 2.0 Pro | 国内直连、性价比高 | 中文场景、合规要求 | 低 |
关键建议:从第一天就做好多模型适配。不是说一开始要接多个模型,而是代码架构要支持切换。
我们项目中的做法——定义统一的 Provider 接口:
// 所有 AI 模型都实现这个接口
interface AIProvider {
name: string;
// 核心能力:给定上下文,生成结果
generateTestCases(
context: GenerationContext,
onProgress?: (progress: number) => void,
): Promise<GeneratedTestCase[]>;
// 通用能力:解析内容
parseContent(systemPrompt: string, content: string): Promise<string>;
}
然后每个模型实现自己的 Provider:
// Claude Provider
class ClaudeProvider extends BaseAIProvider {
name = 'claude';
// ... 具体实现
}
// Doubao Provider
class DoubaoProvider extends BaseAIProvider {
name = 'doubao';
// ... 具体实现
}
这样切换模型只需要改一行配置,不需要改业务代码。
2.2 向量数据库 —— pgvector 一把梭
入门推荐:pgvector(PostgreSQL 扩展)
很多教程会让你单独部署 Milvus、Pinecone、Qdrant 等专用向量数据库。入门阶段完全不需要。
理由:
- 你的业务数据(用户、订单、需求)本来就在 PostgreSQL 里
- pgvector 直接在 PostgreSQL 中添加向量列,不需要额外维护一个数据库
- 对于 10 万条以下的数据量,pgvector 的性能完全够用
-- 就这么简单:给现有表加一个向量列
ALTER TABLE "Requirement" ADD COLUMN embedding vector(1024);
-- 创建索引加速检索
CREATE INDEX ON "Requirement"
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
在 Prisma(Node.js ORM)中使用:
// 语义搜索:找到与查询最相似的需求
const results = await prisma.$queryRawUnsafe(
`SELECT id, title, 1 - (embedding <=> $1::vector) as similarity
FROM "Requirement"
WHERE embedding IS NOT NULL
AND 1 - (embedding <=> $1::vector) >= $2
ORDER BY embedding <=> $1::vector
LIMIT $3`,
vectorStr, // 查询文本的向量
0.65, // 相似度阈值
10, // 返回数量
);
什么时候该换专用向量库?
- 数据量超过 100 万条
- 需要复杂的过滤 + 向量检索组合
- 需要分布式部署
2.3 Agent 框架 —— LangChain 太重?先用原生 SDK
入门推荐:直接用模型 SDK,不用框架。
LangChain/LangGraph 是很好的工具,但对入门者来说:
- 概念太多(Chain、Agent、Tool、Memory、Callback...)
- 抽象层太厚,出了问题很难调试
- 学框架的时间可能比学 Agent 本身还长
更好的路径:先用原生 SDK 理解原理,遇到痛点再引入框架。
// 原生 SDK 调用就够了
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: 'claude-sonnet-4-5-20250929',
max_tokens: 4096,
system: systemPrompt,
messages: [{ role: 'user', content: userMessage }],
}),
});
我们的项目到现在都没用 LangChain,全部是原生 SDK + 自定义 Pipeline。反而更灵活:
Pipeline 流程:
analyzeReqs → decompose → batchGenerate → validate → aggregate
(分析需求) (拆解场景) (批量生成) (验证格式) (汇总结果)
2.4 最小技术栈总结
┌─────────────────────────────────────┐
│ 你的 Agent 应用 │
├─────────────────────────────────────┤
│ 后端: NestJS / Express / FastAPI │
│ 前端: Vue / React / 随便 │
│ 数据库: PostgreSQL + pgvector │
│ AI: 任选一个模型 SDK │
│ 部署: Docker │
└─────────────────────────────────────┘
一句话:PostgreSQL 一把梭,模型 SDK 直连,框架以后再说。
三、核心概念扫盲
这一章用通俗语言解释 Agent 开发中必须理解的 4 个核心概念。
3.1 Embedding —— 把文字变成数字
一句话解释: Embedding 是把一段文字转换成一组数字(向量),使得意思相近的文字对应的数字也相近。
为什么需要?
数据库不理解语义。如果用户搜索"扫码跳转小程序",传统的关键词搜索:
- ✅ 能找到标题包含"扫码"的文档
- ❌ 找不到标题是"二维码识别与小程序唤起"的文档(意思一样,但用词不同)
Embedding 的做法:
- 把"扫码跳转小程序"转成向量
[0.12, -0.34, 0.56, ...] - 把数据库中所有文档都转成向量
- 计算向量之间的距离(余弦相似度)
- 距离最近的就是最相关的
// 调用 Embedding API 把文字变成向量
async generateEmbedding(text: string): Promise<number[]> {
const response = await fetch(embeddingEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: 'doubao-embedding-vision-250615',
input: [{ type: 'text', text: text }],
dimensions: 1024,
}),
});
const data = await response.json();
return data.data.embedding; // [0.12, -0.34, 0.56, ...] 1024维向量
}
常见坑:文本长度限制
Embedding 模型有 token 上限(通常 8000 字符左右)。超过长度怎么办?
// 最简单的处理:截断
const MAX_TEXT_LENGTH = 8000;
const truncatedText = text.length > MAX_TEXT_LENGTH
? text.substring(0, MAX_TEXT_LENGTH)
: text;
更好的方案是分块(chunking):把长文档拆成多个小块,分别生成 embedding。但入门阶段截断就够用。
3.2 RAG —— 让 AI 有记忆、有依据
RAG = Retrieval-Augmented Generation(检索增强生成)
一句话解释: 先从数据库里搜到相关资料,再把资料和用户问题一起喂给 AI,让 AI 基于真实数据回答。
为什么需要?
LLM 有两个致命问题:
- 知识过时:训练数据有截止日期,不知道你公司内部的文档
- 幻觉:不知道的东西会编造
RAG 的解决思路:
用户输入: "扫码跳转小程序的测试用例"
↓
Step 1: 检索(Retrieval)
→ 用 Embedding 在数据库中搜索相似的历史需求和测试用例
→ 找到: "小程序二维码唤起功能需求"(相似度 92%)
→ 找到: "微信扫码跳转测试用例"(相似度 87%)
↓
Step 2: 增强(Augmented)
→ 把检索结果塞进 prompt:
"参考以下历史资料,生成新的测试用例:
[历史需求1]...
[历史用例1]..."
↓
Step 3: 生成(Generation)
→ AI 基于真实资料生成测试用例,而不是凭空编造
我们项目中的 RAG 实现:
// 1. 检索相似需求(语义搜索)
const similarRequirements = await embeddingService.searchSimilar(
'Requirement', // 在需求表中搜索
currentRequirement.text, // 当前需求文本
{ limit: 5, threshold: 0.65, projectId }
);
// 2. 检索相似测试用例(few-shot 示例)
const historicalTestCases = await embeddingService.searchSimilar(
'TestCase',
currentRequirement.text,
{ limit: 3, threshold: 0.7, projectId }
);
// 3. 把检索结果注入到生成上下文中
const context: GenerationContext = {
projectName: '我的项目',
requirements: [currentRequirement.text],
similarRequirements, // RAG: 相似需求,帮助理解业务背景
historicalTestCases, // RAG: 历史用例,作为 few-shot 示例
};
// 4. AI 基于上下文生成
const testCases = await aiService.generateTestCases(model, context);
RAG 的关键参数:
| 参数 | 含义 | 建议值 |
|---|---|---|
threshold | 相似度阈值,低于此值不返回 | 0.60 ~ 0.75 |
limit | 最多返回几条 | 3 ~ 10 |
dimensions | 向量维度 | 1024(平衡精度和性能) |
3.3 Prompt Engineering —— 和 AI 说人话的艺术
一句话解释: 设计好的指令(Prompt),让 AI 准确理解你要什么、按什么格式输出。
三个实战技巧:
技巧 1:结构化 Prompt
不要写一大段自然语言,用 Markdown 结构组织:
const prompt = `你是一个专业的测试用例生成专家。
项目名称: ${projectName}
## 🎯 生成规则
${rulePrompt}
## 📋 需求信息
<requirements>
${requirementText}
</requirements>
## 📚 相似需求参考(RAG 检索)
${similarRequirements}
## 📋 历史用例参考(few-shot 示例)
${historicalTestCases}
## 输出要求
⚠️ 严格要求:只输出 JSON 数组,不要任何其他文字!
直接以 [ 开头,以 ] 结尾。
`;
技巧 2:XML 标签防注入
用 XML 标签包裹用户输入,防止用户内容被模型当作指令:
// 用 <requirements> 标签包裹,AI 会把里面的内容当作"数据"而不是"指令"
prompt += `<requirements>
${userProvidedRequirement}
</requirements>`;
技巧 3:输出约束要反复强调
AI 模型有"话痨"倾向,经常在 JSON 前后加解释文字。解决办法:
prompt += `## 输出要求
⚠️ 严格要求:只输出 JSON 数组,不要任何其他文字!
- 不要写"好的"、"以下是"等开场白
- 不要写任何解释、总结或补充说明
- 直接以 [ 开头,以 ] 结尾`;
3.4 Tool Use —— 让 AI 调用你的函数
一句话解释: 告诉 AI "你有哪些工具可以用",AI 会在需要时主动调用。
这是 Agent 区别于普通 AI 调用的核心能力。以 Claude 为例:
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-5-20250929',
max_tokens: 1024,
tools: [
{
name: 'search_requirements',
description: '搜索历史需求文档',
input_schema: {
type: 'object',
properties: {
query: { type: 'string', description: '搜索关键词' },
limit: { type: 'number', description: '返回数量' },
},
required: ['query'],
},
},
],
messages: [
{ role: 'user', content: '帮我找一下和扫码功能相关的历史需求' }
],
});
// AI 会返回 tool_use 类型的响应,告诉你它想调用哪个工具
// { type: 'tool_use', name: 'search_requirements', input: { query: '扫码功能' } }
MCP(Model Context Protocol) 是 2025 年 Anthropic 推出的工具连接标准,让任何 AI 模型都能用统一的方式连接外部工具。你可以把它理解为"AI 的 USB 接口"。
四、第一个 Agent 项目实战
以"测试用例自动生成"为例,从零搭建一个完整的 Agent 应用。
4.1 项目架构
我们采用 Monorepo 架构,前后端在一个仓库里管理:
ai-test-platform/
├── apps/
│ ├── server/ # NestJS 后端
│ │ ├── src/modules/
│ │ │ ├── ai/ # AI 模型层(多 Provider)
│ │ │ ├── embedding/ # 向量检索层
│ │ │ ├── generation/# 生成 Pipeline
│ │ │ └── ...
│ │ └── prisma/
│ │ └── schema.prisma # 数据库定义
│ └── web/ # Vue 3 前端
│ └── src/views/
├── packages/
│ └── types/ # 共享类型定义
├── pnpm-workspace.yaml
└── turbo.json
为什么用 Monorepo?
- 前后端共享类型定义,不用手动同步
- 一个命令启动所有服务
- 统一的代码审查和版本管理
4.2 核心流程:Pipeline 模式
我们没有用 LangChain,而是自己设计了一个简单的 Pipeline:
┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐
│ 分析需求 │ → │ 拆解场景 │ → │ 批量生成用例 │ → │ 验证格式 │ → │ 汇总结果 │
│ analyzeReqs│ │decompose │ │batchGenerate │ │ validate │ │aggregate │
└──────────┘ └──────────┘ └──────────────┘ └──────────┘ └──────────┘
5% 8% 12~88% 88% 95%
// Pipeline 定义:每个步骤是一个纯函数
const PIPELINE_STEPS = [
{ name: 'analyzeReqs', step: analyzeReqsStep, progress: 5 },
{ name: 'decompose', step: decomposeStep, progress: 8 },
{ name: 'batchGenerate', step: batchGenerateStep, progress: 12 },
{ name: 'validateFmt', step: validateStep, progress: 88 },
{ name: 'aggregate', step: aggregateStep, progress: 95 },
];
// 执行器:依次运行每个步骤
async function runPipeline(state, ctx) {
for (const { name, step, progress } of PIPELINE_STEPS) {
// 检查任务是否被用户取消
const task = await ctx.prisma.generationTask.findUnique({
where: { id: state.taskId },
});
if (task?.status === 'cancelled') return state;
// 更新进度
await ctx.updateStatus(state.taskId, 'generating', progress);
// 执行步骤
state = await step(state, ctx);
}
return state;
}
为什么用 Pipeline 而不是简单的一次调用?
- 拆分复杂度:一次让 AI 生成 100 条用例,质量很差;拆成 10 个场景各生成 10 条,质量好很多
- 可中断恢复:每个步骤之间可以保存状态,用户可以随时取消
- 进度可见:每个步骤更新进度,用户能看到进展
- 独立调试:某个步骤出错了,可以单独调试和重试
4.3 处理 AI 输出的"脏数据"
这是实战中最花时间的部分。 AI 返回的 JSON 经常有问题:
// AI 经常犯的错误:
// 1. 在 JSON 前加"好的,以下是测试用例:"
// 2. 用中文引号 "" 代替英文引号 ""
// 3. JSON 末尾多一个逗号
// 4. 输出被截断(token 限制)
// 我们的处理策略:多层容错
function parseResponse(response: string): TestCase[] {
// Step 1: 提取 JSON 内容(忽略前后的废话)
const firstBracket = response.indexOf('[');
const lastBracket = response.lastIndexOf(']');
let jsonContent = response.substring(firstBracket, lastBracket + 1);
// Step 2: 尝试直接解析
try { return JSON.parse(jsonContent); } catch {}
// Step 3: 修复中文引号
jsonContent = fixChineseQuotes(jsonContent);
try { return JSON.parse(jsonContent); } catch {}
// Step 4: 修复尾部逗号
jsonContent = jsonContent.replace(/,\s*([\]}])/g, '$1');
try { return JSON.parse(jsonContent); } catch {}
// Step 5: 逐对象提取(处理截断的 JSON)
const cases = [];
const objectRegex = /\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g;
let match;
while ((match = objectRegex.exec(jsonContent)) !== null) {
try {
const obj = JSON.parse(match[0]);
if (obj.title) cases.push(obj);
} catch {}
}
return cases;
}
关键经验:永远不要相信 AI 的输出格式是完美的。 多层容错是必须的。
4.4 多模型适配的实战经验
不同模型的输出质量差异很大。我们的数据:
| 模型 | 同一需求生成量 | 覆盖度 | 格式正确率 |
|---|---|---|---|
| Claude 4.5 Sonnet | 60+ 条 | 高 | 98% |
| Doubao 2.0 Pro | 25~30 条 | 中 | 90% |
| GPT-4o | 40+ 条 | 高 | 95% |
应对策略:
- 统一接口:所有模型实现同一个
AIProvider接口 - 配置化:模型选择、API Key 全部存数据库,运行时切换
- 容错增强:JSON 解析做多层 fallback,适配不同模型的输出习惯
// 模型路由:根据任务类型选择最合适的模型
class AIService {
private providers = new Map([
['claude-sonnet-4-5-20250929', ClaudeProvider],
['doubao-seed-2-0-pro-260215', DoubaoProvider],
['gpt-4o', OpenAIProvider],
['gemini-2.0-flash', GeminiProvider],
]);
async getProvider(model: string): Promise<AIProvider> {
const ProviderClass = this.providers.get(model);
const config = await this.getProviderConfig(model);
return new ProviderClass(config);
}
}
五、测试与调试
Agent 应用和传统应用的测试方法有本质区别。
5.1 Agent 的不确定性问题
传统应用:相同输入 → 相同输出(确定性) Agent 应用:相同输入 → 不同输出(非确定性)
这意味着你不能用传统的单元测试断言精确值。
5.2 Agent 测试的三个层次
层次 1:接口级测试(能跑通)
# 最基础:API 能调通,不报错
curl -X POST http://localhost:3000/api/generation/tasks \
-H 'Content-Type: application/json' \
-d '{"projectId": "xxx", "requirementIds": ["yyy"], "model": "claude-sonnet-4-5-20250929"}'
层次 2:输出格式测试(格式对)
// 验证 AI 输出的格式是否符合预期
function validateTestCase(tc: any): boolean {
return (
typeof tc.title === 'string' &&
tc.title.length > 0 &&
Array.isArray(tc.steps) &&
tc.steps.length > 0 &&
['P0', 'P1', 'P2', 'P3'].includes(tc.priority)
);
}
层次 3:质量评估(内容好)
用另一个 AI 来评估生成质量:
// 用思考模型审查生成的测试用例
const reviewReport = await aiService.reviewTestCases(
'claude-sonnet-4-5-20250929', // 审查模型
context, // 原始需求
generatedTestCases, // 生成的用例
scenarios, // 拆解的场景
);
// 审查报告包含:
// - 覆盖度分析
// - 冗余检测
// - 质量评分(1-10)
// - 补充建议
5.3 调试技巧
- 记录完整 Prompt:把发给 AI 的完整 prompt 保存到日志,出问题时可以复现
- 保存原始响应:AI 返回的原始文本要完整保存,方便排查解析问题
- 分步调试:Pipeline 模式让你可以只运行某一个步骤
// 日志记录关键信息
logger.log(`[生成用例] 模型: ${model}`);
logger.log(`[生成用例] Prompt 长度: ${prompt.length}`);
logger.log(`[生成用例] 响应长度: ${response.length}`);
logger.log(`[生成用例] 解析结果: ${testCases.length} 条`);
logger.log(`[性能] 总耗时: ${elapsed}s`);
六、常见坑 & 最佳实践
坑 1:Token 限制导致输出截断
现象: AI 生成到一半突然停了,JSON 不完整。
原因: 模型有 max_tokens 限制,输出超过限制会被截断。
解决:
- 拆分任务(Pipeline 模式,每次只生成一个场景的用例)
- 设置合理的 max_tokens
- JSON 解析做截断容错(逐对象提取)
坑 2:中文引号破坏 JSON
现象: JSON.parse() 报错。
原因: AI 在 JSON 字符串值中使用了中文引号 "" 而不是英文引号 ""。
解决: 解析前替换中文引号:
cleaned = response
.replace(/\u201c/g, '"') // " → "
.replace(/\u201d/g, '"') // " → "
.replace(/\u2018/g, "'") // ' → '
.replace(/\u2019/g, "'"); // ' → '
坑 3:Embedding 长文本截断丢失关键信息
现象: 很长的需求文档,语义搜索结果不准。
原因: Embedding 模型有 token 限制(通常 ~8000 字符),超过的部分直接被截断,后半部分的信息丢失。
解决:
- 短期:截断前做摘要,保留关键信息
- 长期:实现分块策略(chunking),每块独立生成 embedding
坑 4:模型"话太多"
现象: 让 AI 只输出 JSON,它偏要加一段"好的,根据您的需求..."。
解决:
- Prompt 中反复强调输出要求
- 用正则提取
[...]之间的内容 - 在 prompt 末尾加"直接以
[开头"
坑 5:API 超时和重试
现象: AI API 偶尔超时或返回 5xx。
解决: 指数退避重试:
async fetchWithRetry(url, options, retries = 3) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await fetchWithTimeout(url, options, 60000);
if (response.status >= 500 && attempt < retries) {
await delay(1000 * Math.pow(2, attempt - 1)); // 1s, 2s, 4s
continue;
}
return response;
} catch (error) {
if (attempt < retries) {
await delay(1000 * Math.pow(2, attempt - 1));
} else {
throw error;
}
}
}
}
坑 6:依赖安装到错误位置(Monorepo)
现象: pnpm add xxx 后依赖安装了但代码中 import 报错。
原因: 在 monorepo 根目录直接 pnpm add,依赖装到了根目录而不是子项目。
解决: 永远用 --filter:
pnpm --filter server add @nestjs/websockets
pnpm --filter web add naive-ui
总结
Agent 开发的核心路径:
1. 选一个模型 SDK,调通 API ← 30 分钟
2. 搭建基础项目架构(后端 + 数据库) ← 1 天
3. 实现 Embedding + pgvector 语义搜索 ← 1 天
4. 构建 RAG 流程(检索 → 增强 → 生成) ← 2 天
5. 设计 Pipeline + 多层容错 ← 2 天
6. 前端界面 + 进度展示 ← 2 天
总共 1~2 周,你就能拥有一个可用的 Agent 应用。
关键心法:
- 先跑通再优化:不要在技术选型上纠结太久
- Pipeline > 单次调用:拆分任务,分步执行
- 永远不信任 AI 输出:多层容错是必须的
- RAG 是性价比最高的增强方式:让 AI 基于你的数据回答
本文基于一个真实的 AI 测试平台项目的开发经验编写。 如果觉得有帮助,欢迎点赞、收藏、关注专栏。