三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

从RAG到上下文工程:大模型应用落地的核心技术解析与实践

从RAG到上下文工程:大模型应用落地的核心技术解析与实践

1. 从“喂数据”到“喂上下文”:AI工程化的核心范式转变

如果你还在把大模型当成一个简单的问答机,每次对话都从零开始,那你可能已经落后了。过去我们谈AI应用,核心是“喂数据”——准备海量的训练数据,让模型学习。但在大模型时代,尤其是当我们基于API或开源模型进行应用开发时,游戏规则变了。我们不再(或者说很少)去重新训练模型,而是转向了“喂上下文”。这不仅仅是Prompt Engineering(提示词工程)那么简单,它是一套系统工程,关乎如何高效、精准、结构化地将信息“注入”到模型的每一次推理中,从而让模型在特定任务上表现得像一个专家。

“喂上下文”的本质,是弥补大模型在实时性、私有性和精确性上的不足。模型的知识截止于某个时间点,它不知道你公司内部的流程文档;它的回答可能笼统,无法精确引用你提供的技术手册。因此,我们需要将最新的、私有的、精确的信息,作为“上下文”(Context),和用户的“问题”(Query)一起,构成完整的“提示”(Prompt),提交给模型。这听起来简单,但实操起来,从上下文的获取、处理、压缩、组装到最终提交,每一步都充满了工程挑战。这,就是“上下文工程”(Context Engineering)的核心,也是现代AI工程化落地的关键路径。

2. 上下文数据流图:从原始资料到模型输入的完整链路

理解“喂上下文”的第一步,是看清数据是如何流动的。一个完整的上下文工程链路,可以分解为几个核心阶段,它们共同构成了一条高效的数据流水线。

2.1 数据源的获取与加载

上下文不会凭空产生。它的源头多种多样:

  • 非结构化文本:这是最常见的来源,包括PDF技术文档、Word报告、公司Confluence/Wiki页面、网页内容、甚至聊天记录。处理这些数据,首先需要将其从原始格式中提取出纯文本。例如,使用PyPDF2pdfplumber处理PDF,用python-docx处理Word,用BeautifulSoupReadability算法清洗网页。
  • 结构化数据:数据库记录、API返回的JSON、CSV/Excel表格。这些数据本身具有字段结构,但需要转换成模型能理解的叙述性语言或特定格式(如Markdown表格)。例如,将数据库查询结果序列化为一段描述性文字:“当前用户表中有三条记录,分别是:用户A,年龄25;用户B,年龄30...”
  • 代码仓库:当需要模型理解或生成代码时,整个代码库或特定文件就是上下文。这涉及到文件树的解析、关键代码片段的提取,以及跨文件依赖关系的梳理。
  • 多模态数据:虽然当前主流是文本模型,但视觉内容上下文模型也在发展。处理图像、视频时,需要先通过视觉模型(如CLIP)进行理解,生成描述性文本,再将此文本作为上下文喂给语言模型。

注意:数据加载阶段最容易被忽视的是编码和格式问题。确保你的文本加载器能正确处理UTF-8、GBK等各种编码,并过滤掉无意义的乱码和特殊控制字符,这是保证后续环节质量的基础。

2.2 文本的分块与向量化

你不可能把一本500页的书整个塞进模型的上下文窗口。因此,必须对长文本进行“分块”(Chunking)。这不是简单的按字数切割,而是一门学问。

  • 固定长度分块:最简单的方法,如每500个字符(或token)切一块。缺点是可能粗暴地切断一个完整的句子或段落,破坏语义。
  • 基于分隔符的分块:根据自然段落分隔符(如\n\n)、标题(##)、句号等进行切割。这能更好地保持语义完整性。
  • 递归分块:一种更智能的方法。先尝试用大分隔符(如\n\n)分块,如果块还是太大,再用小分隔符(如句号、逗号)继续分,直到块大小符合预设阈值。LangChain等框架提供了RecursiveCharacterTextSplitter工具来实现此逻辑。
  • 语义分块:更高级的方法,利用句子嵌入模型计算相邻句子的相似度,在语义变化处进行切割。这能确保每个块在语义上尽可能内聚。

分块之后,为了能快速从海量块中检索出与用户问题最相关的部分,我们需要将文本块“向量化”。即使用嵌入模型(Embedding Model,如OpenAI的text-embedding-ada-002,或开源的BGESentence-Transformers模型)将每个文本块转换为一个高维向量(一组数字)。这个向量就像是文本的“数学指纹”,语义相近的文本,其向量在空间中的距离也更近。

2.3 检索与相关性排序

当用户提问时,系统需要快速找到最相关的文本块作为上下文。这个过程就是“检索增强生成”(RAG)中的检索(Retrieval)步骤。

  1. 向量相似度检索:将用户问题也向量化,然后计算问题向量与所有文本块向量的余弦相似度或点积,取出相似度最高的前k个块(例如,top-5)。这是最核心的检索方式。
  2. 混合检索:为了兼顾语义匹配和关键词匹配,可以结合传统的全文检索(如BM25算法)。BM25擅长精确匹配关键词,而向量检索擅长语义匹配。将两者的结果按分数融合,能获得更鲁棒的检索效果。
  3. 重排序:初步检索出的top-k个块,其排序可能并非最优。可以使用一个更精细但计算量也更大的“重排序模型”(Reranker),如BGE-Reranker,对这几个候选块进行更精准的相关性打分和重新排序,确保最相关的信息排在最前面。

2.4 上下文的组装与格式化

检索到的文本块,不能直接堆砌起来就扔给模型。我们需要将它们组装成一个结构清晰、模型易于理解的提示。

  • 基础组装:简单地将检索到的文本块用分隔符(如\n---\n)连接起来,前面加上指令:“请根据以下上下文回答问题:”。
  • 结构化组装:对于复杂任务,需要更精细的结构。例如,采用以下格式:
    系统指令(System Prompt): 你是一个技术支持助手,请严格根据提供的文档片段回答问题。 文档片段 1: [检索到的文本块1] 文档片段 2: [检索到的文本块2] ... 用户问题: [用户的实际问题]
  • 引用与溯源:在组装时,为每个文本块添加一个来源标识(如[来源: 用户手册第3章])。这样,在模型生成答案时,可以要求它引用这些来源,既增加了可信度,也方便用户追溯。
  • 处理超长上下文:即使经过检索,有时上下文总长度仍可能超过模型限制(如超过128K token)。这时就需要“上下文压缩”技术。

3. 上下文压缩:在有限窗口内塞入无限信息的艺术

模型上下文窗口再大(如Claude 3的200K,DeepSeek的128K),面对真正的海量资料库也是杯水车薪。更关键的是,随着上下文增长,模型处理中间信息的能力会下降,出现“中间丢失”现象。因此,上下文压缩成为高级RAG系统的必备技能。

3.1 为什么需要压缩?不只是长度限制

压缩的目的有三个:

  1. 突破长度限制:这是最直接的原因。
  2. 降低计算成本:输入模型的token数直接关联着API调用费用和计算延迟。
  3. 提升信息密度与质量:去除冗余、无关信息,让模型专注于最精华的部分,反而能提升回答质量。

3.2 主流压缩策略与实践

  • 提取式摘要:这是最直观的方法。使用另一个(通常是更小、更快的)模型,对长文本进行摘要,保留核心事实和实体。例如,用gpt-3.5-turbo来总结检索到的文档块,再将摘要作为上下文。但风险在于摘要过程可能丢失关键细节。
  • 抽象式重写:让模型基于检索到的上下文和原始问题,重新组织、改写出一段更精炼、更直接针对问题的背景信息。这比简单摘要更智能,但成本也更高。
  • 选择性上下文:不压缩内容本身,而是压缩数量。通过更精细的检索和重排序,只选取置信度最高的1-2个片段,而不是5个。这要求检索系统非常精准。
  • 层次化压缩:这正是网络热词中提到的“Claude Code上下文分层”思路。对于超长文档(如代码库),先构建一个高层级的索引(如目录树、模块说明),当用户提问具体问题时,先检索高层级索引定位到相关模块,再深入该模块检索详细代码。这模拟了人类阅读大型文档的方式。
  • 智能过滤与去重:在组装上下文前,对检索到的片段进行去重(基于向量或文本),并过滤掉与问题相关性极低的片段(通过设置相似度阈值)。

3.3 Claude的“压缩上下文”命令:一个启发

网络热词中提到了“claude code压缩上下文命令”。虽然我们无法得知其具体实现,但这给了我们一个工程启示:压缩可以是一个交互式、可引导的过程。我们可以设计专门的“压缩提示词”,让模型自己来执行压缩。例如,给模型的指令可以是:

你是一个上下文压缩专家。我将给你一段长文本和一个核心问题。你的任务是从长文本中提取出所有与回答该问题直接相关的事实、数据和关键句子,并组织成一段连贯、简洁的摘要。忽略所有无关的背景介绍、例子和冗余解释。 长文本:[此处放入需要压缩的文本] 核心问题:[用户的问题] 请开始压缩:

通过这种方式,我们将压缩任务本身也“外包”给了大模型,实现了动态的、基于查询的上下文优化。

4. Prompt的精密组装:系统指令、上下文与用户查询的三角舞

上下文准备好了,如何与Prompt结合,是决定模型输出质量的临门一脚。一个健壮的Prompt模板,通常包含三个部分:系统指令(System Prompt)、上下文(Context)和用户查询(User Query)。它们各司其职,共同引导模型。

4.1 系统指令:设定角色与行为边界

系统指令在对话开始时一次性给定,用于塑造模型的“人格”和回答范式。它与“function call”的区别在于,系统指令是战略性的、风格性的,而function call是战术性的、结构化的API调用。

  • 角色设定:“你是一位资深Linux系统运维专家,回答专业、准确、简洁。”
  • 输出格式约束:“请用Markdown格式输出,先给出结论,再分点阐述原因。”
  • 安全与边界:“你只能根据我提供的上下文回答问题。如果上下文不包含相关信息,请明确说‘根据提供的信息,我无法回答此问题’,切勿杜撰。”
  • 处理流程说明:“在回答前,先简要复述你从上下文中找到的关键证据。”

一个强大的系统指令,能极大减少后续对话中的“调教”成本。你需要像产品经理设计交互规范一样,去精心设计系统指令。

4.2 上下文的嵌入技巧

上下文如何放入Prompt,直接影响模型对它的“关注度”。

  • 位置很重要:将最重要的上下文放在靠近用户问题的地方。研究表明,模型对Prompt开头和结尾的信息记忆更深刻(首因效应和近因效应)。可以考虑把核心证据放在最后。
  • 使用明确的标记:用如<context>...</context>## 参考文档 ##这样的标记将上下文包裹起来,与指令和问题清晰区分。
  • 添加引导性指令:在上下文前后加上指令,如“请仔细阅读以下背景信息:”和“基于以上背景,请回答:”,主动引导模型去“使用”上下文。
  • 处理多个来源:当上下文来自多个不相关的文档时,明确分隔并标注来源,避免模型混淆。

4.3 用户查询的优化

用户的原生问题往往是模糊的。在将其送入Prompt前,可以进行“查询重写”或“查询扩展”。

  • 查询重写:用模型将“它怎么工作的?”这种模糊问题,结合对话历史,重写成更具体的“根据之前讨论的X系统,请解释其数据同步模块的工作流程。”
  • 查询扩展:生成原问题的同义词或相关问题,用于在向量检索时召回更多相关片段。例如,对“如何备份数据库?”可以扩展出“数据库备份步骤”、“backup database method”、“数据备份方案”等。

4.4 一个完整的Prompt组装示例

系统指令: 你是一个IT知识库助手。你的回答必须严格基于提供的“参考文档”内容。如果文档中没有答案,请说“文档中未提及”。回答请清晰,并引用文档标题。 参考文档: 【文档标题:服务器安装指南-v2.1】 1. 安装前需确保系统为Ubuntu 20.04或更高版本。 2. 运行安装脚本的命令是:`sudo ./install.sh --mode=prod`。 3. 安装完成后,默认服务端口为8080。 【文档标题:常见问题排查】 若端口8080被占用,安装脚本会自动尝试8081端口。 用户查询: 我在Ubuntu 22.04上运行安装脚本,应该用什么命令?

这个组装清晰地划分了责任:系统指令定规则,上下文提供弹药,用户查询提出问题。

5. 工程实践中的核心挑战与应对策略

理论很美好,但实践起来坑很多。下面分享几个在构建“喂上下文”系统时最常见的挑战和我的应对经验。

5.1 上下文噪声与幻觉抑制

最大的风险是,检索系统可能返回不相关或轻微相关的文档片段,这些“噪声”会干扰模型,甚至导致它基于错误信息生成看似合理但完全错误的答案(幻觉)。

  • 策略1:提高检索精度:这需要投入精力优化嵌入模型、分块策略和检索算法。有时,简单的调整分块大小(从500调到300)或尝试不同的嵌入模型,就能显著提升效果。
  • 策略2:设置置信度阈值:为向量检索的相似度分数设置一个阈值(例如,只保留相似度>0.7的片段)。低于阈值的片段,宁可不用,也不要引入噪声。
  • 策略3:让模型自我验证:在Prompt中要求模型:“请判断以下上下文是否足以回答该问题?如果不足以,请指出缺失什么信息。”这相当于让模型做一次质量检查。
  • 策略4:输出引用与溯源:强制模型在答案中引用上下文片段的编号。这样,如果答案有问题,我们可以快速定位到是哪个片段提供了错误信息,从而反向优化我们的知识库。

5.2 长上下文下的性能衰减

即使上下文在窗口限制内,模型对中间部分信息的理解和记忆能力也会下降。

  • 策略:关键信息重复与强调:对于最核心的指令或信息,可以在Prompt的开头和结尾都提及一次。或者在上下文中,用加粗、标题等形式突出关键句子。
  • 策略:结构化与分节:将长上下文用清晰的标题(如“## 一、背景 ##”、“## 二、配置步骤 ##”)组织起来,这有助于模型建立内部索引,更好地定位信息。

5.3 动态上下文与多轮对话

在聊天应用中,上下文不仅包括检索到的文档,还包括历史对话记录。如何管理不断增长的对话历史?

  • 策略:滑动窗口:只保留最近N轮对话作为上下文。这是最简单有效的方法。
  • 策略:历史摘要:在对话轮数增多时,用一个模型对之前的对话历史进行摘要,然后用摘要+最近几轮对话作为新的上下文。这需要在对话过程中异步调用模型进行摘要,工程复杂度较高,但能保留更长的记忆。
  • 策略:关键记忆提取:尝试从历史对话中提取出关键实体(如讨论的产品名、决定的方案、提到的数字)和用户偏好,将这些结构化信息作为上下文,而不是完整的对话记录。

5.4 工具调用(Function Calling)与上下文的结合

当模型需要执行具体操作(查询天气、执行计算、调用数据库)时,需要将工具的描述作为上下文喂给模型。这就是“系统Prompt与Function Call区别”的体现。系统Prompt是总纲,而Function Call的描述是具体的“工具说明书”。

  • 最佳实践:将可用工具的详细描述(名称、功能、输入参数JSON Schema、输出示例)清晰地放在上下文中。当模型决定调用工具时,它会产生一个符合Schema的JSON输出,你的后端程序解析这个JSON再去真正调用API。这实现了模型与外部世界的安全、结构化交互。

6. 构建你自己的上下文工程流水线:从工具选型到部署

纸上得来终觉浅,我们来勾勒一个最小可行上下文工程系统的搭建思路。

6.1 核心组件选型

  • 向量数据库:这是存储和检索文本向量的核心。轻量级可选ChromaDBFAISS(本地库),生产环境考虑WeaviateQdrantPinecone(云服务)。选择时考虑易用性、性能、过滤查询能力。
  • 嵌入模型:开源首选BGE系列(如BAAI/bge-large-zh)或Sentence-Transformers模型。英文场景text-embedding-ada-002仍是标杆。关键是要做评测,看它在你的领域数据上的表现。
  • 文本分块工具LangChainRecursiveCharacterTextSplitter是个不错的起点,可以基于它定制自己的分块逻辑。
  • 大语言模型API:根据需求选择。高精度选GPT-4/GPT-4o,性价比选Claude 3 Haiku或DeepSeek-V2,私有化部署选开源模型如Qwen、GLM,通过LM StudioOllama等工具本地运行。

6.2 流水线搭建步骤

  1. 知识库预处理(离线)

    • 收集所有文档(PDF、Word、网页等)。
    • 编写脚本,使用相应的解析库提取纯文本。
    • 应用分块策略,将长文本切成大小合适的片段。
    • 使用嵌入模型将每个文本块转化为向量。
    • (文本块, 向量, 元数据[如来源、页码])存储到向量数据库中。
  2. 用户查询处理(在线)

    • 接收用户问题。
    • (可选)对问题进行查询重写/扩展。
    • 使用相同的嵌入模型将问题向量化。
    • 在向量数据库中执行相似度搜索,检索出Top-K个相关文本块。
    • (可选)对检索结果进行重排序。
  3. Prompt组装与调用

    • 按照预设的模板,将系统指令、检索到的上下文、用户问题组装成最终Prompt。
    • 调用大语言模型API,发送Prompt。
    • 接收模型回复,解析并返回给用户(同时可解析其中的引用信息)。

6.3 一个简单的代码示例(概念层面)

以下是一个使用Python和LangChain框架的简化示例,展示核心流程:

# 注意:此为概念示例,需安装相应库并填写API密钥 from langchain_community.document_loaders import TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma from langchain.chains import RetrievalQA from langchain_community.llms import OpenAI # 1. 加载与分块 loader = TextLoader("knowledge_base.txt") documents = loader.load() text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50) texts = text_splitter.split_documents(documents) # 2. 向量化与存储 embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh") vectorstore = Chroma.from_documents(texts, embeddings, persist_directory="./chroma_db") # 3. 构建检索链 llm = OpenAI(api_key="your_key", temperature=0) # 或使用其他LLM qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", # 简单地将所有上下文塞入Prompt retriever=vectorstore.as_retriever(search_kwargs={"k": 4}), chain_type_kwargs={"prompt": YOUR_CUSTOM_PROMPT} # 这里可以注入精心设计的Prompt模板 ) # 4. 提问 result = qa_chain.run("什么是AI工程化中的上下文?") print(result)

6.4 迭代与评估

系统搭建完成后,必须建立评估机制。

  • 构建测试集:收集一批真实用户可能问的问题,并准备好标准答案或关键要点。
  • 评估指标
    • 检索相关性:人工或用小模型判断检索到的上下文是否真的相关。
    • 答案准确性:对比模型答案与标准答案。
    • 答案忠实度:判断答案是否严格源自上下文,有无幻觉。
    • 用户体验:回答是否流畅、清晰。
  • 持续优化:根据评估结果,回头调整分块大小、嵌入模型、检索数量、Prompt模板等各个环节的参数和策略。

“给模型喂上下文”不是一个一蹴而就的魔法,而是一个需要持续迭代和打磨的工程系统。它介于传统的软件工程和机器学习之间,要求开发者既懂数据流程和系统架构,又理解大模型的行为特性。当你看到模型能够精准地引用你公司内部文档来回答复杂问题时,你就会明白,这一切的工程努力都是值得的。这不再是简单的调用API,而是真正在构建属于你自己的、具备深度领域知识的智能体。

← 返回列表