本章目标:用最通俗的语言,讲清楚 Agent 开发中绕不开的四个核心概念。不需要你有机器学习背景,只要你写过 CRUD,就能看懂。
3.1 Embedding —— 把文字变成"坐标"
一句话解释
Embedding 就是把一段文字转换成一组数字(向量),让计算机能用数学距离来衡量两段文字的语义相似度。
为什么需要它?
假设你的数据库里有 500 条历史需求,用户输入了一条新需求"扫码跳转小程序"。你想找出以前做过的类似需求作为参考。
传统关键词搜索的问题:
- "扫码跳转小程序" vs "扫描二维码打开微信小程序" → 关键词不完全匹配,搜不到
- "扫码跳转小程序" vs "小程序码生成" → 关键词部分匹配,但语义完全不同,搜出来是噪音
Embedding 的解法: 把两段文字都转成向量,计算向量之间的距离。语义相近的文字,向量距离就近。
"扫码跳转小程序" → [0.12, -0.34, 0.56, ...] (1536维向量)
"扫描二维码打开微信小程序" → [0.13, -0.33, 0.55, ...] ← 距离很近!语义相似
"小程序码生成" → [0.78, 0.12, -0.45, ...] ← 距离很远,语义不同
实际怎么用?
调用大模型厂商的 Embedding API,传入文本,返回向量。以下是一个真实的调用示例:
// 调用 Embedding API 生成向量
async function generateEmbedding(text: string): Promise<number[]> {
// 关键:文本要做截断,大部分 Embedding 模型有 token 上限
const MAX_TEXT_LENGTH = 8000;
const truncatedText = text.length > MAX_TEXT_LENGTH
? text.substring(0, MAX_TEXT_LENGTH)
: text;
const response = await fetch(embeddingEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: 'doubao-embedding', // 或 text-embedding-3-small (OpenAI)
input: [truncatedText]
})
});
const data = await response.json();
return data.data[0].embedding; // 返回 number[] 向量
}
向量存到哪? 存到 PostgreSQL 的 vector 类型字段(需要装 pgvector 扩展):
-- 启用 pgvector 扩展
CREATE EXTENSION IF NOT EXISTS vector;
-- 给表加向量字段
ALTER TABLE requirements ADD COLUMN embedding vector(1536);
-- 相似度查询(核心 SQL)
SELECT id, title,
1 - (embedding <=> $1::vector) AS similarity
FROM requirements
WHERE 1 - (embedding <=> $1::vector) >= 0.65 -- 相似度阈值
ORDER BY similarity DESC
LIMIT 10;
<=>是 pgvector 的余弦距离运算符。1 - 距离 = 相似度,值越大越相似。
入门要记住的三件事
- Embedding 有长度限制 —— 大部分模型支持 512~8192 tokens,超长文本要截断
- 不同模型的向量不互通 —— OpenAI 生成的向量不能和 Doubao 的做比较
- 相似度阈值需要调 —— 0.65 是个不错的起点,但要根据实际数据测试
3.2 RAG —— 让模型"开卷考试"
一句话解释
RAG(Retrieval-Augmented Generation,检索增强生成)就是在调用大模型之前,先从数据库里检索相关资料,塞进 prompt 里一起发给模型。让模型带着"参考资料"回答问题,而不是纯靠记忆。
为什么需要它?
大模型有两个天然缺陷:
- 知识截止 —— 训练数据有截止日期,不知道你公司的业务逻辑
- 幻觉 —— 不知道的事情会瞎编,而且编得很像真的
RAG 的解法很直接:你不知道没关系,我把资料给你,你照着资料回答。
一个完整的 RAG 流程
用户输入 "扫码跳转小程序" 的需求
↓
① 生成用户输入的 Embedding
↓
② 在向量数据库中搜索相似内容
├── 找到相似需求:"微信扫码跳转"(相似度 0.82)
├── 找到历史用例:"扫码功能测试用例"(相似度 0.75)
└── 找到知识库页面:"小程序跳转规范文档"
↓
③ 把检索结果 + 用户输入 组装成 Prompt
↓
④ 发给大模型生成回答
在代码里长什么样?
// 语义搜索:输入文本 → 返回相似内容
async function semanticSearch(queryText: string) {
// 1. 把查询文本转成向量
const queryEmbedding = await generateEmbedding(queryText);
// 2. 搜索相似的需求
const similarRequirements = await findSimilarRequirements(queryEmbedding, {
threshold: 0.6, // 相似度阈值
limit: 5 // 最多返回5条
});
// 3. 搜索相似的历史测试用例
const historicalTestCases = await findSimilarTestCases(queryEmbedding, {
threshold: 0.65,
limit: 3
});
return { similarRequirements, historicalTestCases };
}
然后把搜索结果注入到 prompt 里:
function buildPromptWithRAG(userInput: string, ragResults) {
let prompt = `你是一个专业的测试用例生成专家。\n`;
// 注入相似需求(标注"仅供参考",防止模型照抄)
if (ragResults.similarRequirements.length > 0) {
prompt += `\n## 相似需求参考(仅供理解上下文,不要直接复制)\n`;
for (const req of ragResults.similarRequirements) {
prompt += `- ${req.title}(相似度: ${(req.similarity * 100).toFixed(0)}%)\n`;
prompt += ` ${req.description}\n`;
}
}
// 注入历史用例(作为风格参考)
if (ragResults.historicalTestCases.length > 0) {
prompt += `\n## 历史测试用例参考(参考风格和粒度,不要复制内容)\n`;
for (const tc of ragResults.historicalTestCases) {
prompt += `- ${tc.title}\n 步骤: ${tc.steps}\n`;
}
}
prompt += `\n## 当前需求\n${userInput}\n`;
prompt += `\n请基于以上信息生成测试用例。`;
return prompt;
}
入门要记住的三件事
- RAG 不是万能的 —— 检索质量直接决定生成质量,垃圾进垃圾出
- "仅供参考"很重要 —— 一定要在 prompt 里告诉模型参考资料是参考,不是让它照抄
- 先跑通,再优化 —— 一开始用最简单的"全文截断 + 余弦相似度"就够了,别一上来就搞复杂的分块策略
3.3 Prompt Engineering —— 怎么"说人话"让模型干活
一句话解释
Prompt Engineering 就是精心设计发给大模型的指令文本,让它的输出更准确、更稳定、更符合你的预期。
为什么这很重要?
同样的任务,不同的 prompt 写法,输出质量天差地别:
❌ 差的 prompt:"帮我写测试用例"
→ 模型不知道写什么功能的、什么格式的、什么粒度的用例
✅ 好的 prompt:
"你是一个专业的测试用例生成专家。
项目名称: 电商小程序
## 生成规则
- 测试用例必须包含前置条件、操作步骤、预期结果
- 优先级分为 P0/P1/P2 三个级别
- 必须覆盖正常流程、异常流程、边界条件
## 当前需求
用户扫描商品二维码,跳转到对应的小程序商品详情页
请生成测试用例,输出 JSON 格式。"
实战中的 Prompt 结构
一个生产级的 prompt 通常包含这些层次:
┌─────────────────────────────────────┐
│ ① 角色定义 │ "你是一个专业的测试用例生成专家"
├─────────────────────────────────────┤
│ ② 规则约束(最重要,放最前面) │ 用户选择的生成规则
├─────────────────────────────────────┤
│ ③ RAG 上下文 │ 相似需求、历史用例、知识库
├─────────────────────────────────────┤
│ ④ 用户输入 │ 当前要处理的需求
├─────────────────────────────────────┤
│ ⑤ 输出格式要求 │ JSON schema、字段说明
└─────────────────────────────────────┘
关键经验: 规则约束要放在 prompt 的前面,因为大模型对开头的指令遵从度最高。
一些实用技巧
1. 用 XML 标签包裹重要内容
<rules>
以下是用户选择的生成规则,你必须严格遵守:
1. 每个测试用例必须关联需求 ID
2. 步骤数不少于 3 步
</rules>
模型对结构化标签的遵从度明显高于纯文本。
2. 给"反面示例"
⚠️ 禁止生成以下类型的场景:
- "全量测试"、"综合测试"等模糊场景
- 与需求无关的通用测试项
告诉模型不要做什么,比只告诉它要做什么更有效。
3. Few-shot:给几个例子
## 参考示例(学习风格和粒度,不要复制内容)
示例用例 1:
标题:正常扫码跳转 - 有效二维码
步骤:1. 打开扫码功能 → 2. 扫描有效商品码 → 3. 等待页面加载
预期:成功跳转到商品详情页,显示正确商品信息
给 2-3 个真实例子,比写 10 行规则更管用。
入门要记住的三件事
- Prompt 是代码,不是随便写的文案 —— 要版本管理、要测试、要迭代
- 位置很重要 —— 最重要的指令放开头,模型对开头最敏感
- 不同模型对同一个 prompt 的表现不同 —— Claude 和 Doubao 对同一个 prompt 的输出质量可能差很大,需要分别调试
3.4 Tool Use —— 让模型能"动手"
一句话解释
Tool Use(也叫 Function Calling)是让大模型不只是"说",还能"做"的能力。你预先定义好一组工具(函数),模型在需要时会主动调用这些工具来完成任务。
为什么需要它?
大模型擅长理解和生成文本,但它做不了这些事:
- 查数据库
- 调用 API
- 读写文件
- 执行计算
Tool Use 让模型可以说:"我需要调用 查询订单 这个工具,参数是 order_id=12345",然后你的代码去执行,把结果返回给模型,模型再继续推理。
工作流程
用户:"帮我查一下订单 12345 的物流状态"
↓
模型推理:我需要查订单信息
↓
模型输出:调用工具 get_order_status(order_id="12345")
↓
你的代码:执行 get_order_status,返回 { status: "已发货", tracking: "SF1234" }
↓
模型继续:订单 12345 已发货,顺丰快递单号 SF1234
在代码里怎么实现?
以 Claude 的 Tool Use 为例,你需要定义工具的 JSON Schema:
const tools = [{
name: 'generate_test_cases',
description: '根据需求生成结构化的测试用例',
input_schema: {
type: 'object',
properties: {
// 要求模型先分析需求,再生成用例(强制思考)
requirements_analysis: {
type: 'object',
properties: {
total_requirements: { type: 'number' },
requirement_summaries: {
type: 'array',
items: {
type: 'object',
properties: {
requirement_id: { type: 'string' },
summary: { type: 'string' },
key_scenarios: { type: 'array', items: { type: 'string' } }
}
}
}
}
},
// 实际的测试用例输出
test_cases: {
type: 'array',
items: {
type: 'object',
properties: {
title: { type: 'string' },
requirementId: { type: 'string' },
steps: {
type: 'array',
items: {
type: 'object',
properties: {
order: { type: 'number' },
action: { type: 'string' },
expectedResult: { type: 'string' }
}
}
},
priority: { type: 'string', enum: ['P0', 'P1', 'P2'] }
}
}
}
},
required: ['requirements_analysis', 'test_cases']
}
}];
// 调用时指定强制使用工具
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-5-20250929',
max_tokens: 64000,
tools: tools,
tool_choice: { type: 'tool', name: 'generate_test_cases' }, // 强制调用
messages: [{ role: 'user', content: prompt }]
});
关键技巧:
tool_choice: { type: 'tool', name: '...' }可以强制模型使用指定工具,确保输出是结构化 JSON,而不是自由文本。
Tool Use vs 直接让模型输出 JSON
| 对比项 | 直接输出 JSON | Tool Use |
|---|---|---|
| 格式稳定性 | 经常格式错误,需要大量容错 | Schema 约束,格式几乎不出错 |
| 流式支持 | 需要自己做增量 JSON 解析 | 原生支持 input_json_delta 流 |
| 复杂结构 | 嵌套深了容易出错 | Schema 天然支持嵌套 |
| 适用场景 | 简单输出(一段文本) | 结构化数据(表格、表单、API 响应) |
入门要记住的三件事
- Tool Use 的本质是结构化输出 —— 不一定要"调用外部工具",纯粹当结构化输出约束用也很香
- Schema 设计很重要 —— required 字段一定要标,不然模型可能跳过
- 流式 Tool Use 要处理增量 JSON —— 模型会一块一块吐出 JSON 片段,你需要拼接后再解析
本章小结
| 概念 | 一句话 | 什么时候用 |
|---|---|---|
| Embedding | 文字→向量,算距离 | 需要语义搜索、相似度匹配时 |
| RAG | 检索+生成,开卷考试 | 模型需要用你的私有数据回答时 |
| Prompt Engineering | 精心设计指令 | 每次调用模型都要用 |
| Tool Use | 让模型输出结构化数据 | 需要可靠的 JSON/表格输出时 |
这四个概念互相配合:用 Embedding 做检索,用 RAG 把检索结果注入 Prompt,用 Prompt Engineering 让模型理解任务,用 Tool Use 保证输出格式。这就是一个 Agent 的核心工作流。
下一章,我们会用一个真实项目,把这些概念串起来实现。