1. 三年前的"智能客服":以"推荐问题卡片"为中心的交互范式
大约在 2023 年前后(以 2026 年回看约三年前),多数企业平台的"智能客服"已经成为标配。但当时的智能客服形态与今天的"自然对话式 Agent"不完全一样,典型产品打开后,会出现类似截图所示的对话框首页:

- 左侧是"了解产品"一类的分类入口
- 右侧是"推荐解决方案"一类的引导入口
- 每个入口下方给出若干条高频问题(FAQ/场景问题)
- 用户点击任意一条问题,系统会自动在对话窗口中展示一段标准答案,并可继续引导下一步操作
这种设计在当时非常常见,原因很现实:
- 降低用户输入成本:用户不需要组织语言,点一下就能得到结果。
- 降低系统理解难度:后台可以把点击行为直接映射到"固定意图(Intent)/固定答案"。
- 风险更可控:输出来自知识库的标准内容,避免"自由生成"导致不一致或不合规。
- 运营更容易:产品经理可以通过后台配置卡片与答案,做 A/B、调整排序、做热点问题运营。
但随着业务复杂度上升,这类"卡片式 FAQ"会遇到一个瓶颈:用户真实表达往往不是标准问题,而是自然语言的"变体"。
2. 为什么必须引入向量(Embedding):因为用户表达是自然语言的"海量变体"
当用户不点卡片、而是直接输入时,问题就来了:
- "ECS 有什么优势?"
- "云服务器性能怎么样?"
- "我想上云买主机,怎么选?"
- "弹性计算适合什么场景?"
它们本质上可能指向同一类知识,但字面差异很大。
如果你仍然用三年前常见的做法(关键词匹配/正则/简单意图分类),会出现典型问题:
- 同义表达覆盖困难:需要维护大量词典与规则。
- 语序与口语化导致命中率下降:用户一句话里掺杂背景、抱怨、目标。
- 长文本与多意图难处理:一句话包含多个诉求,规则体系容易崩。
- 冷启动成本高:新产品、新功能上线,规则与训练集需要重新积累。
而 Embedding 的价值在于:
它把自然语言映射为高维向量,使"语义相似"变成可计算的距离(比如余弦相似度)。
于是后台可以从"字符串相等/关键词命中"升级为:
输入问题 → 向量化 → 与知识库问题/段落向量做相似度检索 → 返回最相关的答案
这一步,就是从"卡片式智能客服"走向"语义检索式智能客服"的关键拐点。
3. 从"卡片点击直出答案"到"语义检索返回答案":系统结构怎么变
你可以这样描述演进路径(非常贴合实际落地):
-
阶段 A:纯卡片/纯意图
- 点击卡片 = 命中固定意图 = 返回固定答案
-
阶段 B:卡片 + 文本输入,但规则匹配为主
- 用户输入 → 关键词/规则 → 命中 FAQ 条目
-
阶段 C:Embedding 驱动的语义检索(你这段代码对应的能力)
- 用户输入 → Embedding → TopK 相似问题/相似段落 → 返回答案/拼接上下文
-
阶段 D:RAG/对话式生成
- 在阶段 C 的基础上,把召回内容交给 LLM 进行更自然的组织与回答(同时保留引用与可追溯)
你的代码正好就是阶段 C 的"最小闭环 Demo"。
4. 代码引入:Embedding 相似度检索 Demo(C#)

C#
using System.ClientModel;
using OpenAI;
using OpenAI.Embeddings;
Console.WriteLine("=== Embedding Similarity Demo ===\n");
//var apiKey = Environment.GetEnvironmentVariable("AI__EmbeddingApiKey");
var apiKey = "*****************************";
var endpoint =
// "https://personalopenai1.openai.azure.com/openai/v1/";
"https://dashscope.aliyuncs.com/compatible-mode/v1/";
//var deploymentName = "text-embedding-3-large";
var deploymentName = "text-embedding-v4";
if (string.IsNullOrEmpty(apiKey))
{
Console.WriteLine("Error: AI__EmbeddingApiKey environment variable not set.");
return;
}
// Initialize embedding client
var openAiClient = new OpenAIClient(
new ApiKeyCredential(apiKey),
new OpenAIClientOptions { Endpoint = new Uri(endpoint) }
);
var embeddingClient = openAiClient.GetEmbeddingClient(deploymentName);
// Sample texts on different topics
var sampleTexts = new[]
{
"C# 是一种在数据科学和机器学习领域广泛使用的编程语言。",
"我喜欢用新鲜的番茄和罗勒来烹饪意大利面。",
"这场足球比赛非常精彩,最终比分是 3 比 2。",
"机器学习算法可以在大型数据集中识别模式。",
"这个食谱需要两杯面粉和三个鸡蛋。",
"篮球运动需要良好的协调能力和团队合作精神。",
"神经网络的设计灵感来源于生物大脑结构。",
"在家烘焙面包需要耐心以及合适的温度。",
"这支足球队经过数月训练后赢得了冠军。",
"深度学习已经彻底改变了计算机视觉和自然语言处理领域。"
};
Console.WriteLine("Generating embeddings for sample texts...\n");
// Generate and store embeddings
var textEmbeddings = new List<(string text, float[] embedding)>();
foreach (var text in sampleTexts)
{
ClientResult<OpenAIEmbedding> embeddingResult = await embeddingClient.GenerateEmbeddingAsync(text);
float[] embedding = embeddingResult.Value.ToFloats().ToArray();
textEmbeddings.Add((text, embedding));
Console.WriteLine($"✓ {text}");
}
Console.WriteLine(
$"\nStored {textEmbeddings.Count} text embeddings (dimension: {textEmbeddings[0].embedding.Length})\n");
// Interactive query loop
while (true)
{
Console.WriteLine("\n" + new string('-', 70));
Console.Write("Enter your query (or 'quit' to exit): ");
var query = Console.ReadLine();
if (string.IsNullOrWhiteSpace(query) || query.ToLower() == "quit")
{
Console.WriteLine("Goodbye!");
break;
}
Console.WriteLine($"\nSearching for: \"{query}\"");
Console.WriteLine("Generating query embedding...");
// Generate embedding for query
var queryEmbeddingResult = await embeddingClient.GenerateEmbeddingAsync(query);
var queryEmbedding = queryEmbeddingResult.Value.ToFloats().ToArray();
Console.WriteLine(queryEmbedding.Length);
Console.WriteLine(string.Join(',', queryEmbedding));
// Calculate similarities
var similarities = new List<(string text, double similarity)>();
foreach (var (text, embedding) in textEmbeddings)
{
var similarity = CalculateCosineSimilarity(queryEmbedding, embedding);
similarities.Add((text, similarity));
}
// Sort by similarity (highest first)
var topResults = similarities.OrderByDescending(x => x.similarity).Take(3).ToList();
Console.WriteLine("\nTop 3 Most Similar Texts:");
Console.WriteLine(new string('=', 70));
for (int i = 0; i < topResults.Count; i++)
{
var (text, similarity) = topResults[i];
Console.WriteLine($"\n{i + 1}. Similarity: {similarity:F4} ({similarity * 100:F2}%)");
Console.WriteLine($" Text: {text}");
}
}
// Helper function to calculate cosine similarity
static double CalculateCosineSimilarity(float[] vector1, float[] vector2)
{
if (vector1.Length != vector2.Length)
{
throw new ArgumentException("Vectors must have the same dimension");
}
double dotProduct = 0;
double magnitude1 = 0;
double magnitude2 = 0;
for (int i = 0; i < vector1.Length; i++)
{
dotProduct += vector1[i] * vector2[i];
magnitude1 += vector1[i] * vector1[i];
magnitude2 += vector2[i] * vector2[i];
}
magnitude1 = Math.Sqrt(magnitude1);
magnitude2 = Math.Sqrt(magnitude2);
if (magnitude1 == 0 || magnitude2 == 0)
{
return 0;
}
return dotProduct / (magnitude1 * magnitude2);
}