
在开发 AI Agent 的过程中有一个问题几乎无法回避大模型训练数据是静态的而业务场景里的知识和权限是动态的。如果 Agent 只能靠模型记忆回答问题遇到私有文档、实时数据或者专业术语时就会出现答不上来、答不对甚至乱答的情况。RAGRetrieval-Augmented Generation检索增强生成是目前缓解这类问题最成熟的方案之一也是把外部知识接入 Agent 的标准姿势。本文是「使用 Rust 开发 AI Agent」系列的第 08 篇重点讲清 RAG 的核心概念并用 Rust 从零实现一个最小可运行的 RAG 引擎包括文档切块、Embedding 调用、向量检索、上下文拼接和 LLM 生成。适合已经掌握 Rust 基础、想了解 Agent 知识库闭环的读者也适合刚接触 RAG 想快速看代码的开发者。在正式写代码之前先花一点篇幅把 RAG 的概念和流程讲清楚。很多人一开始就扎进向量数据库和 Embedding 参数里结果代码跑通了但不知道为什么检索结果不相关也不知道该往哪个方向调优。下面我们把原理先拆明白。1. 为什么 AI Agent 需要 RAG1.1 AI Agent 的典型知识缺口AI Agent 的核心能力是“感知、规划、记忆、工具使用和行动”其中“记忆”是一个非常容易被低估的模块。大模型本身有参数记忆但参数记忆有几个先天限制。第一训练数据有时间截止模型无法天然知道最新的事件、价格、接口文档或内部制度。第二企业的私有文档、代码仓库、数据库字段说明通常不会进入模型训练语料模型根本没有见过。第三大模型在不确定时倾向于“流畅地编造”这种问题在技术术语多、事实细节强的场景里尤其致命。如果 Agent 在回答用户问题时只能依赖 LLM 自身的记忆那它就和一个没有联网、没有资料、只能凭经验答题的实习生一样。遇到熟悉的题目能答对遇到稍微偏门或内部专属的内容就会出错。要解决这个问题就要给 Agent 增加一条“外部知识读取”的链路这正是 RAG 出现的背景。1.2 RAG 是什么RAG 的全称是 Retrieval-Augmented Generation中文通常翻译为“检索增强生成”。它的思路并不复杂在让大模型生成答案之前先从外部知识库中检索出与问题最相关的文本片段把这些片段作为“参考资料”拼到提示词中再让大模型基于这些资料生成回答。用一个通俗的类比来解释RAG 相当于把原本的“闭卷考试”变成“开卷考试”。模型不需要记住所有知识只需要学会快速查资料并基于查到的资料组织答案。这样做有三个明显好处答案内容可以实时更新知识来源可以追溯模型“不懂装懂”的概率会明显下降。从专业定义上说RAG 是一个由“索引Indexing”“检索Retrieval”“生成Generation”三部分组成的系统。索引负责把外部文档转成可检索的形式检索负责找到与问题最匹配的片段生成负责把检索结果和原始问题合并后交给 LLM 得到最终答案。在 AI Agent 架构中RAG 通常作为 Agent 的一个知识工具其它工具还有搜索、计算器、数据库查询等。1.3 RAG 与微调的区别很多人会把 RAG 和模型微调Fine-tuning放在一起比较。微调是修改模型本身的参数让模型“学会”新的知识或行为RAG 是不修改模型参数通过外部检索临时补充知识。两者的对比如下表所示。对比维度RAG微调知识更新直接替换文档即可成本低需要重新训练模型成本高数据要求需要完整的知识文档需要构造问答对或指令数据幻觉抑制较好因为答案有检索依据取决于训练数据质量和规模可追溯性强可以指出依据来自哪篇文档弱难以解释模型为什么这样回答依赖资源需要向量检索基础设施需要 GPU 训练资源适合场景企业知识库、实时信息、文档问答特定格式输出、领域风格适配实际上RAG 和微调并不是二选一的关系。在工业级项目中常见做法是先通过微调让模型学会某种回答格式和语气再通过 RAG 注入实时知识。两者可以配合使用。对于绝大多数 AI Agent 的场景优先考虑 RAG 通常是性价比更高的选择。2. RAG 的核心工作流程2.1 离线索引阶段RAG 系统分为“离线索引”和“在线检索生成”两个阶段。离线索引阶段做的事情是把原始文档变成可供检索的向量数据。整个过程包括文档加载、文本清洗、切块、Embedding 向量化、写入向量存储五个步骤。原始文档可能是 Markdown、PDF、Word、HTML 或数据库记录。首先要做的是把不同格式的内容提取为纯文本然后去掉无意义的时间戳、页眉页脚、超链接等噪声。接着由于一篇文档整体向量化会丢失细节通常要把长文本切成若干片段这叫“切块”或“分块”Chunking。每个片段经过 Embedding 模型转换为一个稠密向量最后把向量和原始文本一起存储到向量数据库中形成“知识库索引”。离线索引阶段的特点是“一次构建、多次使用”。它不需要实时运行可以在文档更新后重新执行也可以做成定时任务。对于 Rust 开发者来说这个阶段可以独立成 CLI 工具或后台服务这也符合 Rust 擅长做高性能批处理的特点。2.2 在线检索与生成阶段当用户提出一个问题时系统会进入在线检索与生成阶段。流程可以拆成四步。第一步把用户问题输入同一个 Embedding 模型转换成查询向量。这里要注意问题向量和文档向量必须使用同一个模型生成否则向量空间不一致相似度计算没有意义。第二步把查询向量拿到向量存储中进行相似度检索得到 Top-K 个最相关的文档片段。第三步把这几个片段拼接成“参考资料”连同系统提示词、用户问题一起组成 Prompt。第四步调用 LLM 生成最终答案。这个流程看起来简单但每个环节都有可调点。例如是返回 3 个片段还是 5 个片段上下文过长会不会干扰模型判断参考资料之间要不要按相关度排序这些都会影响最终回答质量。RAG 工程化的核心就是在这些细节里做权衡。2.3 关键子问题切块策略与向量检索切块策略是影响 RAG 效果的第一大因素。如果切块太大一个片段可能包含多个主题检索时召回的内容不够聚焦如果切块太小单个片段语义不完整大模型无法理解上下文。常见的做法是按段落或句子边界切块同时让相邻片段保留少量重叠避免关键信息被切断在边界上。对于中文文档还要注意按字符切分按字节切分容易把 UTF-8 字符截断成乱码。向量检索方式也需要选择。最朴素的是暴力线性扫描计算查询向量与所有文档向量的余弦相似度。数据量小的时候完全够用代码也简单。数据量超过百万条之后才需要考虑 HNSW、IVF 等近似最近邻索引或者直接使用 Qdrant、Milvus、pgvector 这类专业向量数据库。本文的示例为了演示原理会自己实现一个基于余弦相似度的简易向量存储这样不依赖外部数据库也能把 RAG 流程跑通。另外近两年经常提到的 Agentic RAG本质是在 RAG 流程中引入 Agent 的自主决策能力例如让 Agent 决定要检索哪几个知识库、是否要二次检索、是否调用工具进行补充查询。但这属于 RAG 的进阶形态本文先聚焦基础闭环。3. 环境准备Rust 开发环境与演示项目结构3.1 Rust 工具链安装在开始写 RAG 代码之前先确认 Rust 工具链已经安装好。Rust 官方推荐使用 rustup 管理工具链。Linux 和 macOS 上通常执行以下命令安装curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | shWindows 上可以直接从官网下载 rustup-init.exe或者在已有 PowerShell 环境中执行安装程序。安装完成后打开新的终端窗口运行下面两条命令验证rustc --version cargo --version如果能看到类似rustc 1.xx.0和cargo 1.xx.0的信息说明工具链正常。版本号会随发布时间变化本文示例以稳定版工具链为准重点演示结构和思路不依赖特定小版本特性。在配置 Rust 环境时有读者会遇到下载速度不稳定或更新失败的问题。rustup 支持通过环境变量RUSTUP_DIST_SERVER和RUSTUP_UPDATE_ROOT指定更新源。设置成可访问的镜像地址后再执行rustup update stable可以有效缓解下载超时问题。不过具体镜像地址需要根据你的网络情况选择这里不展开特定配置。3.2 Windows 下不使用 MSVC 的注意事项Windows 上安装 Rust 时rustup 默认会使用x86_64-pc-windows-msvc工具链它依赖 Visual Studio Build Tools 中的 MSVC 链接器。如果不想安装体积庞大的 Visual Studio可以选择 GNU 工具链。具体做法是安装 GNU 工具链并把它设为默认rustup toolchain install stable-x86_64-pc-windows-gnu rustup default stable-x86_64-pc-windows-gnu如果你已经在用 MSVC 工具链也可以用rustup set default-host x86_64-pc-windows-gnu修改默认宿主。使用 GNU 工具链后Cargo 在链接.rlib和原生库时会调用 MinGW-w64 的链接器而不是 MSVC 的link.exe。这意味着如果项目依赖某些包含 C/C 原生代码的 crate例如llama.cpp的绑定你需要额外准备好 MinGW-w64 环境。简单总结不使用 MSVC 是可行的方案就是切换为 GNU ABI 的 toolchain。对于纯 Rust 编写的项目这个切换过程很平滑对于需要系统原生依赖的项目要注意链接器的可用性。本文的 RAG 示例依赖较少不涉及本地 C 库编译因此两种工具链都可以运行。3.3 演示项目结构本文的 Rust RAG 演示项目文件结构设计如下。它刻意保持精简方便你把每个模块单独拿出来理解。rag-agent-demo/ ├── Cargo.toml └── src/ ├── main.rs # 程序入口负责流程编排 ├── chunking.rs # 文档切块 ├── embedding.rs # Embedding API 客户端 ├── vector_store.rs # 简易向量存储与相似度检索 └── agent.rs # RAG Agent组装检索与生成这个拆分方式与实际项目是吻合的。切块、向量化、存储、检索、生成分别对应 RAG 链路中的不同职责。后续如果要替换成专业向量数据库或者接入不同的 Embedding 服务只需要修改对应模块的内部实现上层逻辑不用大改。4. 使用 Rust 实现一个最小 RAG 引擎4.1 创建工程与依赖先创建一个新的 Cargo 项目cargo new rag-agent-demo cd rag-agent-demo然后打开Cargo.toml添加依赖。这里使用了tokio作为异步运行时reqwest发送 HTTP 请求调用 Embedding 和 LLM 接口serde和serde_json处理 JSON 序列化anyhow简化错误处理。[package] name rag-agent-demo version 0.1.0 edition 2021 [dependencies] tokio { version 1, features [full] } serde { version 1, features [derive] } serde_json 1 reqwest { version 0.12, default-features false, features [json, rustls-tls] } anyhow 1这里有一个值得说明的点reqwest默认使用系统原生 TLS 库在 Windows 上有时会因为 OpenSSL 缺失或冲突导致编译失败。关闭默认特性并改用rustls-tls可以避免很多本机环境问题因为 rustls 是一个纯 Rust 实现的 TLS 库编译和运行都更省心。如果你的项目需要连接必须走原生 TLS 的代理环境再按需调整。同时本文的 Embedding 模型和 LLM 都通过“OpenAI 兼容 HTTP 接口”调用。这样做的好处是你可以对接本地部署的 Ollama、llama.cpp、vLLM也可以对接云厂商的兼容接口。不同的本地推理方案通常都提供了/v1/embeddings和/v1/chat/completions端点接口形态一致。4.2 文档切块模块创建一个文件src/chunking.rs定义文档片段结构体并实现按字符长度切块、带重叠的逻辑。这里的核心是把文本按 UTF-8 字符边界切分避免把中文字符截断。// 文件路径src/chunking.rs pub struct Chunk { pub id: usize, pub source: String, pub content: String, } /// 将文本按最大字符数切块相邻块之间保留 overlap 个字符的重叠。 pub fn chunk_text(text: str, max_chars: usize, overlap: usize) - VecString { assert!(max_chars overlap, max_chars 必须大于 overlap); let chars: Vecchar text.chars().collect(); if chars.is_empty() { return Vec::new(); } let mut chunks Vec::new(); let mut start 0; let step max_chars - overlap; while start chars.len() { let end (start max_chars).min(chars.len()); let piece: String chars[start..end].iter().collect(); chunks.push(piece); if end chars.len() { break; } start step; } chunks } /// 将一个文档切成多个 Chunk带来源标识。 pub fn chunk_document( source: str, content: str, max_chars: usize, overlap: usize, ) - VecChunk { chunk_text(content, max_chars, overlap) .into_iter() .enumerate() .map(|(id, text)| Chunk { id, source: source.to_string(), content: text, }) .collect() }这段代码的关键是text.chars()。Rust 的字符串本身是 UTF-8 字节序列直接对字节下标切片很容易在中文等多字节字符中间截断。转换为Vecchar后按字符计数再重新收集为String可以保证切出来的每一段都是完整的字符。设置重叠overlap的意义在于如果两个相关句子刚好落在切块边界的两边没有重叠就会导致检索时丢失关键信息。实际项目中重叠大小通常设置为 50 到 200 字符之间具体需要根据文档类型调整。4.3 Embedding 客户端创建src/embedding.rs实现一个调用 OpenAI 兼容 Embedding 接口的客户端。它负责把一批文本转换成向量内部处理了 HTTP 请求、鉴权和响应解析。// 文件路径src/embedding.rs use anyhow::{bail, Result}; use serde::Deserialize; #[derive(Clone)] pub struct EmbeddingClient { endpoint: String, api_key: String, model: String, http: reqwest::Client, } #[derive(Deserialize)] struct EmbeddingResponse { data: VecEmbeddingData, } #[derive(Deserialize)] struct EmbeddingData { embedding: Vecf32, } impl EmbeddingClient { pub fn new( endpoint: impl IntoString, api_key: impl IntoString, model: impl IntoString, ) - Self { Self { endpoint: endpoint.into().trim_end_matches(/).to_string(), api_key: api_key.into(), model: model.into(), http: reqwest::Client::new(), } } pub async fn embed(self, texts: [String]) - ResultVecVecf32 { let url format!({}/v1/embeddings, self.endpoint); let body serde_json::json!({ model: self.model, input: texts, }); let mut req self.http.post(url); if !self.api_key.is_empty() { req req.bearer_auth(self.api_key); } let resp req.json(body).send().await?; if !resp.status().is_success() { let status resp.status(); let text resp.text().await.unwrap_or_default(); bail!(embedding 请求失败: status{}, body{}, status, text); } let parsed: EmbeddingResponse resp.json().await?; let mut result Vec::with_capacity(parsed.data.len()); for item in parsed.data { result.push(item.embedding); } Ok(result) } }这个客户端把api_key设成可选如果为空字符串就不附加 Authorization 头。这样既能对接需要鉴权的云端服务也能方便地对接本地无需鉴权的推理服务。trim_end_matches(/)是为了防止用户在配置地址时多写一个斜杠导致 URL 拼接错误。从设计上看这个EmbeddingClient是比较通用的。如果以后要切换成其他 Embedding 服务只需要改embed方法里的请求格式和响应解析。Rust 的类型系统在这里帮我们保证了“所有 Embedding 返回的向量维度是一致的”这一隐含约束类型不匹配会在编译阶段暴露。4.4 向量存储与相似度检索创建src/vector_store.rs。这里不引入外部向量数据库而是直接用Vec保存文档片段和向量再通过余弦相似度做线性检索。在数据量几百条的演示场景下性能完全够用。// 文件路径src/vector_store.rs use crate::chunking::Chunk; pub struct VectorStore { pub chunks: VecChunk, pub embeddings: VecVecf32, } impl VectorStore { pub fn new() - Self { Self { chunks: Vec::new(), embeddings: Vec::new(), } } pub fn add(mut self, chunk: Chunk, embedding: Vecf32) { self.chunks.push(chunk); self.embeddings.push(embedding); } pub fn len(self) - usize { self.chunks.len() } pub fn is_empty(self) - bool { self.chunks.is_empty() } /// 返回 Top-K 最相关片段的下标和相似度得分。 pub fn search(self, query: [f32], top_k: usize) - Vec(usize, f32) { let mut scores: Vec(usize, f32) self .embeddings .iter() .enumerate() .map(|(idx, emb)| { let score cosine_similarity(query, emb); (idx, score) }) .collect(); scores.sort_by(|a, b| b.1.partial_cmp(a.1).unwrap_or(std::cmp::Ordering::Equal)); scores.truncate(top_k); scores } } pub fn cosine_similarity(a: [f32], b: [f32]) - f32 { if a.len() ! b.len() { return 0.0; } let mut dot 0.0; let mut norm_a 0.0; let mut norm_b 0.0; for i in 0..a.len() { dot a[i] * b[i]; norm_a a[i] * a[i]; norm_b b[i] * b[i]; } if norm_a 0.0 || norm_b 0.0 { return 0.0; } dot / (norm_a.sqrt() * norm_b.sqrt()) }余弦相似度衡量的是两个向量方向的接近程度值域在 -1 到 1 之间。对于归一化后的 Embedding 向量余弦相似度和内积结果是等价的。这里没有对向量做归一化所以计算时需要分别求模长。在实际向量数据库中很多系统会预先对向量归一化这样查询时只需要计算点积性能更高。search方法先把所有候选得分收集到一个Vec中再降序排序并截取前top_k条。这个实现的时间复杂度是 O(N log N)当样本数极大时效率不够但用于理解原理和中小规模知识库没有问题。如果后续数据量大可以替换成 HNSW 索引或引入专门向量数据库。4.5 Agent 查询编排创建src/agent.rs把检索结果和 LLM 生成拼接成一个完整的 RAG Agent。这个模块是整个示例的核心它决定了“查到的资料”如何变成“最终答案”。// 文件路径src/agent.rs use anyhow::{anyhow, bail, Result}; use serde::Deserialize; use crate::embedding::EmbeddingClient; use crate::vector_store::VectorStore; #[derive(Deserialize)] struct ChatResponse { choices: VecChatChoice, } #[derive(Deserialize)] struct ChatChoice { message: ChatMessage, } #[derive(Deserialize)] struct ChatMessage { content: String, } pub struct RagAgent { embedding: EmbeddingClient, store: VectorStore, llm_endpoint: String, llm_model: String, llm_key: String, http: reqwest::Client, } impl RagAgent { pub fn new( embedding: EmbeddingClient, store: VectorStore, llm_endpoint: impl IntoString, llm_model: impl IntoString, llm_key: impl IntoString, ) - Self { Self { embedding, store, llm_endpoint: llm_endpoint.into().trim_end_matches(/).to_string(), llm_model: llm_model.into(), llm_key: llm_key.into(), http: reqwest::Client::new(), } } pub async fn query(self, question: str) - ResultString { // 1. 将问题向量化 let question_vec self.embedding.embed([question.to_string()]).await?; let query_embedding question_vec[0]; // 2. 从知识库中检索 Top-3 片段 let hits self.store.search(query_embedding, 3); if hits.is_empty() { return Ok(知识库中没有检索到相关内容。.to_string()); } let context hits .iter() .map(|(idx, _score)| self.store.chunks[*idx].content.clone()) .collect::Vec_() .join(\n\n); // 3. 拼接系统提示词和用户问题 let system 你是一个 Rust 技术助手。请主要根据提供的参考资料回答问题不要编造知识。如果参考资料不足以回答请明确说明。; let user format!(参考资料\n{}\n\n问题{}, context, question); // 4. 调用 LLM 生成答案 let url format!({}/v1/chat/completions, self.llm_endpoint); let body serde_json::json!({ model: self.llm_model, messages: [ {role: system, content: system}, {role: user, content: user} ], temperature: 0.2 }); let mut req self.http.post(url); if !self.llm_key.is_empty() { req req.bearer_auth(self.llm_key); } let resp req.json(body).send().await?; if !resp.status().is_success() { let status resp.status(); let text resp.text().await.unwrap_or_default(); bail!(LLM 请求失败: status{}, body{}, status, text); } let parsed: ChatResponse resp.json().await?; parsed .choices .into_iter() .next() .map(|c| c.message.content) .ok_or_else(|| anyhow!(LLM 响应中没有 choices)) } }query方法完整覆盖了 RAG 在线阶段的核心链路。第一行先把用户问题转成向量这一步如果省略整个检索就无法进行。检索时使用top_k 3也就是说最终回答最多参考 3 个片段这个值可以根据知识库质量和上下文窗口长度调整。在 Prompt 设计上系统提示词明确要求模型“根据参考资料回答不要编造知识”。这是 RAG 减少幻觉的关键手段之一。temperature设置为 0.2是为了让回答更偏向确定性和事实性而不是创造性发散。4.6 主程序与运行验证最后创建src/main.rs组装整个流程。我们准备了几段关于 Rust 和 AI Agent 的模拟文档然后执行切块、向量化、入库最后提出一个关于“借用规则”的问题观察 RAG 能否基于知识库回答。// 文件路径src/main.rs mod agent; mod chunking; mod embedding; mod vector_store; use agent::RagAgent; use chunking::chunk_document; use embedding::EmbeddingClient; use vector_store::VectorStore; #[tokio::main] async fn main() - anyhow::Result() { // 1. 加载文档 let documents load_documents(); println!(加载文档数量: {}, documents.len()); // 2. 切块 let mut all_chunks Vec::new(); for (source, content) in documents { let chunks chunk_document(source, content, 80, 10); all_chunks.extend(chunks); } println!(切块数量: {}, all_chunks.len()); // 3. 生成 Embedding 并写入向量存储 let embedding_endpoint std::env::var(EMBEDDING_ENDPOINT) .unwrap_or_else(|_| http://localhost:11434.to_string()); let embedding_key std::env::var(EMBEDDING_API_KEY).unwrap_or_default(); let embedding_model std::env::var(EMBEDDING_MODEL) .unwrap_or_else(|_| nomic-embed-text.to_string()); let embedding EmbeddingClient::new(embedding_endpoint, embedding_key, embedding_model); let texts: VecString all_chunks.iter().map(|c| c.content.clone()).collect(); let vectors embedding.embed(texts).await?; let mut store VectorStore::new(); for (chunk, vector) in all_chunks.into_iter().zip(vectors) { store.add(chunk, vector); } println!(向量库记录数: {}, store.len()); // 4. 构造 RAG Agent let llm_endpoint std::env::var(LLM_ENDPOINT) .unwrap_or_else(|_| http://localhost:11434.to_string()); let llm_model std::env::var(LLM_MODEL) .unwrap_or_else(|_| qwen2.5:7b.to_string()); let llm_key std::env::var(LLM_API_KEY).unwrap_or_default(); let agent RagAgent::new(embedding, store, llm_endpoint, llm_model, llm_key); // 5. 提问并输出答案 let question Rust 的借用规则是什么; println!(问题: {}, question); let answer agent.query(question).await?; println!(回答:\n{}, answer); Ok(()) } fn load_documents() - Vec(String, String) { vec![ ( rust_intro.md.to_string(), Rust 是一门系统编程语言由 Mozilla 发起强调内存安全、并发安全和性能。Rust 的所有权系统在编译期检查内存使用避免空指针、悬垂引用和数据竞争等问题。.to_string(), ), ( rust_ownership.md.to_string(), Rust 的所有权规则每一个值在同一时刻只能有一个所有者当所有者离开作用域时值会被自动释放将值赋值给另一个变量会转移所有权而不是浅拷贝。.to_string(), ), ( rust_borrow.md.to_string(), Rust 的借用规则在任意时刻要么只能有一个可变引用要么可以有多个不可变引用引用必须始终有效不能悬垂借用不会转移所有权。这些规则让编译器可以在不引入垃圾回收的前提下保证内存安全。.to_string(), ), ( ai_agent.md.to_string(), AI Agent 是一种能感知环境、自主规划、调用外部工具并最终完成任务执行的智能体。在实际应用中Agent 经常需要结合 RAG 来获取实时或私有知识缓解大模型知识陈旧和幻觉问题。.to_string(), ), ] }运行前你需要准备一个本地或远程的 Embedding 服务和 LLM 服务。如果使用 Ollama可以提前拉取对应的模型然后确认默认服务端口11434可用。如果使用其他兼容服务通过环境变量覆盖默认地址即可例如export EMBEDDING_ENDPOINThttp://your-embedding-host:port export EMBEDDING_MODELyour-embedding-model export LLM_ENDPOINThttp://your-llm-host:port export LLM_MODELyour-llm-model执行cargo run后预期输出大致如下加载文档数量: 4 切块数量: 6 向量库记录数: 6 问题: Rust 的借用规则是什么 回答: 根据参考资料Rust 的借用规则是在任意时刻要么只能有一个可变引用要么可以有多个不可变引用引用必须始终有效不能悬垂借用不会转移所有权。如果环境配置正常你应该能看到模型根据知识库中的rust_borrow.md片段给出答案而不是凭空发挥。这说明从切块、向量化、检索到生成的最小闭环已经跑通。5. 常见问题与排查思路在实践 RAG 开发时遇到问题是很正常的。下面这张表汇总了 Rust 环境、接口调用、检索效果三类问题中最常见的情况。问题现象常见原因解决思路Cargo 编译时报linking with cc failedWindows 下缺少 MSVC 或 GNU 链接器安装 VS Build Tools或切换为 GNU 工具链reqwest 编译 TLS 相关错误系统缺少 native-tls 依赖或冲突改用default-features falserustls-tls特性Embedding 接口返回 401API Key 没有配置或配置错误检查服务的鉴权要求确认 Authorization 头是否发送Embedding 接口返回 404