LLM上下文工程实战:从RAG到系统优化,构建高效AI应用
1. 项目概述为什么“上下文工程”值得你投入精力如果你是一名AI应用开发者、产品经理或者只是对如何更好地与大型语言模型LLM打交道感兴趣那么“上下文工程”这个概念很可能就是你当前效率瓶颈的突破口。我最初接触这个概念是在为一个复杂的客服自动化系统设计提示词Prompt时发现无论怎么优化单条指令模型在处理多轮、长链条的对话时表现总是不稳定。直到我开始系统性地思考“上下文”的构建与管理整个项目的效果才有了质的飞跃。“Meirtz/Awesome-Context-Engineering”这个项目就是一个围绕“上下文工程”这一新兴领域的资源精选列表。它不是一个可以直接运行的代码库而是一个“元资源”——一个帮你高效找到所有相关工具、论文、框架和最佳实践的导航图。在LLM应用开发中上下文Context指的是你提供给模型的全部输入信息包括系统指令、用户查询、历史对话、检索到的知识片段等。如何设计、组织、优化和管理这些信息以最低的成本尤其是Token成本激发模型的最佳性能这就是上下文工程要解决的核心问题。这个资源库的价值在于它帮你跳过了在浩如烟海的论文、博客和开源项目中盲目搜寻的过程直接指向了当前领域最前沿、最实用的成果。无论你是想了解基础理论如思维链、Few-Shot Learning还是寻找落地方案如LangChain的上下文压缩、LlamaIndex的检索增强或是研究最新的学术突破如推理中的“System 2”思考这个列表都能为你提供一个清晰的起点。接下来我将结合自己的实践经验为你深度拆解如何利用这类资源并构建起你自己的上下文工程知识体系与实践框架。2. 上下文工程的核心价值与挑战解析2.1 超越提示词工程从单点优化到系统工程很多人对LLM的调优还停留在“提示词工程”阶段即精心雕琢一句指令。这固然重要但在复杂应用中远远不够。举个例子我开发过一个法律文档分析助手用户会上传一份几十页的合同并提出一系列问题。如果每次提问都把整份合同作为上下文喂给模型成本极高且模型容易“迷失”在信息海洋中无法聚焦于当前问题相关的条款。上下文工程正是为了解决这类问题。它要求我们以系统化的视角来处理信息流信息筛选与压缩不是把所有数据都塞给模型而是先进行智能过滤。比如使用嵌入模型Embedding和向量数据库只检索与当前问题最相关的几个合同段落。结构设计与编排以什么样的顺序和组织方式呈现信息是把历史问答放在前面还是后面系统指令、用户身份、工具调用结果应该如何排列不同的编排方式会显著影响模型的“注意力”分配。动态管理与演化在多轮对话中上下文窗口是有限的如128K Tokens。如何决定哪些历史对话需要保留哪些可以摘要或丢弃这需要一套动态的上下文管理策略。实操心得不要一开始就追求复杂的架构。我的建议是先从记录和可视化你的上下文开始。在开发初期把每次调用模型的实际输入包括所有隐藏的系统消息、历史记录完整地打印或保存下来。你会惊讶地发现你以为的上下文和模型实际“看到”的上下文可能存在巨大差异。这是优化工作的第一步。2.2 直面核心挑战成本、长度与幻觉的三角博弈在实践中上下文工程始终在平衡一个“不可能三角”效果、成本和长度。效果Effectiveness我们希望模型给出的答案尽可能准确、相关、符合指令。成本CostAPI调用费用与输入的Token数量直接相关。无节制地扩展上下文意味着高昂的账单。长度Length/Window模型有其固定的上下文窗口限制。虽然窗口在不断扩大从4K到128K甚至更多但长上下文下的性能衰减“中间丢失”现象和推理速度下降仍是问题。此外“幻觉”问题也与上下文密切相关。当模型无法从上下文中找到确凿依据时它更倾向于“捏造”答案。因此一个核心的上下文工程目标就是为模型的每一次推理提供恰好足够、高度相关、结构清晰的证据和信息。常见误区认为“给的上下文越多越好”。实际上过多的无关信息就是噪声会稀释关键信息的重要性导致模型表现下降同时白白增加成本。这好比在考试时给学生的参考资料不是一本重点明确的复习提纲而是整个图书馆的藏书目录。3. 核心技术栈与工具选型指南“Awesome-Context-Engineering”列表中会涵盖大量工具我们可以将其归类为几个核心的技术方向方便你根据需求进行选型。3.1 检索增强生成为模型配备“外部记忆”检索增强生成是上下文工程的基石技术。它的核心思想是不让模型死记硬背所有知识而是在需要时从一个外部的知识库如文档、数据库中实时检索相关信息并将其作为上下文提供给模型。核心组件与选型考量文本分割器如何将长文档切成适合检索的片段Chunk考量点按字符/Token数分割最简单但可能切断句子或段落语义。更优的方案是使用语义分割如semantic-text-splitter或递归地按标点、换行进行分割。我的选择对于技术文档我常用基于Markdown标题的递归分割能很好地保持文档结构。对于普通文本LangChain的RecursiveCharacterTextSplitter配合适当的分隔符列表如\n\n,\n,.,!,?,,是稳妥的起点。嵌入模型将文本片段转化为计算机可以理解的向量一组数字。考量点需要在效果、速度和成本间权衡。开源模型如BAAI/bge-small-zh-v1.5部署灵活但需要自建服务。云API如OpenAI的text-embedding-3-small简单易用但有调用延迟和费用。参数注意嵌入向量的维度如768, 1536影响存储和计算开销。并非维度越高越好要与你的数据规模和检索精度要求匹配。实操技巧为中文场景选择专门优化的双语或中文嵌入模型至关重要。直接用英文模型处理中文检索效果会大打折扣。向量数据库存储和快速检索这些向量。轻量级/原型ChromaDB、FAISS本地库。适合快速验证想法ChromaDB的易用性尤其突出。生产级/云服务Pinecone、Weaviate、Qdrant。它们提供托管服务解决了可扩展性、持久化和多节点查询的问题。如果你的数据量会持续增长或需要高并发访问应优先考虑这类方案。选型关键除了基础的相似性搜索余弦相似度关注数据库是否支持过滤如按元数据过滤、混合搜索结合关键词和向量等高级功能这些在实际应用中非常实用。3.2 上下文优化与压缩技术当检索到的相关片段仍然很多或者对话历史很长时我们需要对上下文进行“瘦身”。摘要压缩做法用另一个LLM通常是更小、更快的模型对长文本进行摘要然后将摘要而非原文放入上下文。工具LangChain内置了LLMChainExtractor等摘要压缩器。你可以指定一个压缩提示词如“请将以下文本总结为保留核心事实的简短段落”。适用场景处理冗长的历史对话或对检索到的长文档进行初步浓缩。选择性上下文做法基于当前查询动态选择历史对话中最相关的部分进行保留而非全部保留。原理这通常通过计算当前查询与历史每轮对话的相似度来实现保留最相关的N轮。挑战如何定义“相关”简单的向量相似度可能不够有时需要更复杂的逻辑如最近几轮对话通常更重要。上下文窗口管理策略滑动窗口只保留最近N个Token的对话。最简单但可能丢失重要的早期信息。关键记忆缓存识别并永久或在较长时间内缓存对话中的关键事实如用户的名字、项目偏好。这需要更复杂的逻辑来识别“关键信息”。工具实现一些新兴的框架开始原生支持这些策略关注LlamaIndex的TokenTextSplitter和ChatMemoryBuffer的高级配置或LangChain的ConversationSummaryBufferMemory等记忆组件。3.3 高级模式与框架智能体与工具调用上下文不仅是文本还可以是模型调用工具如计算器、搜索引擎、API后返回的结果。如何将工具的描述、调用结果清晰、结构化地嵌入上下文是智能体流畅工作的关键。OpenAI的Function Calling和Anthropic的Tool Use为此提供了标准范式。思维链与推理框架通过设计上下文引导模型进行逐步推理。例如在上下文中加入“让我们一步步思考”的指令或提供几个“Few-Shot”的推理示例。更复杂的框架如ReActReason Act会在上下文中交替呈现模型的“思考”和“行动”形成一种与外部环境交互的循环。多模态上下文未来的上下文不限于文本。如何将图像、音频的描述或特征向量有效地组织到上下文中并与文本信息协同是一个前沿方向。这通常涉及多模态大模型或专门的融合架构。4. 从零搭建一个上下文优化系统的实操流程下面我将以一个“智能技术文档问答系统”为例展示如何应用上述技术栈构建一个完整的、经过上下文优化的应用。假设我们的文档是某个开源软件的Markdown格式手册。4.1 第一阶段数据准备与索引构建这是离线阶段目标是构建一个高效的知识检索库。步骤1文档加载与清洗# 使用 LangChain 的文档加载器 from langchain_community.document_loaders import DirectoryLoader, UnstructuredMarkdownLoader loader DirectoryLoader(./docs/, glob**/*.md, loader_clsUnstructuredMarkdownLoader) raw_documents loader.load() # 清洗移除多余的HTML标签、空白字符等 import re def clean_text(text): text re.sub(r\n{3,}, \n\n, text) # 合并多个空行 text re.sub(r\s, , text).strip() # 合并多余空格 return text for doc in raw_documents: doc.page_content clean_text(doc.page_content)注意清洗策略因数据源而异。对于Markdown保留#、**等格式符号有时对后续的语义分割有益但过多的无关字符如复杂的HTML必须清除。步骤2智能文本分割from langchain.text_splitter import RecursiveCharacterTextSplitter, MarkdownHeaderTextSplitter from langchain.schema import Document # 方案A基于Markdown标题的递归分割更适合结构清晰的文档 headers_to_split_on [ (#, Header 1), (##, Header 2), (###, Header 3), ] markdown_splitter MarkdownHeaderTextSplitter(headers_to_split_onheaders_to_split_on) md_header_splits [] for doc in raw_documents: splits markdown_splitter.split_text(doc.page_content) # 保留元数据如来源文件 for split in splits: split.metadata.update(doc.metadata) md_header_splits.extend(splits) # 方案B通用递归字符分割保底方案 text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个块的目标字符数 chunk_overlap50, # 块之间的重叠字符避免语义割裂 separators[\n\n, \n, . , ! , ? , , , , ] # 分割优先级 ) final_splits text_splitter.split_documents(md_header_splits) # 对方案A的结果进行二次细分 print(f原始文档数: {len(raw_documents)}) print(f分割后块数: {len(final_splits)})关键参数解析chunk_size500这是一个起点。对于密集的技术文档300-600可能更合适对于叙述性文字800-1000也行。需要通过检索效果来调优。chunk_overlap50重叠是为了防止一个完整的句子或概念被硬生生切开。重叠部分通常是chunk_size的10%-20%。实操心得不要迷信单一分割策略。我通常会尝试2-3种不同的chunk_size和分割器然后用一组标准问题测试哪种组合的检索效果最好。这是一个需要实验的环节。步骤3向量化与入库from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma # 使用开源的嵌入模型需提前下载模型 embeddings HuggingFaceEmbeddings( model_nameBAAI/bge-small-zh-v1.5, # 针对中文优化的模型 model_kwargs{device: cpu}, # 根据环境选择 cuda encode_kwargs{normalize_embeddings: True} # 归一化方便使用余弦相似度 ) # 创建向量数据库 vectorstore Chroma.from_documents( documentsfinal_splits, embeddingembeddings, persist_directory./chroma_db # 指定持久化目录 ) vectorstore.persist() # 保存到磁盘选型替代如果选择Pinecone代码会有所不同主要是初始化客户端和索引。生产环境务必关注索引的维度是否与嵌入模型输出维度匹配以及距离度量余弦相似度、点积等的选择。4.2 第二阶段查询时上下文组装与优化这是在线阶段当用户提问时动态发生。步骤1基础检索与上下文组装from langchain.chains import RetrievalQA from langchain_openai import ChatOpenAI # 初始化LLM llm ChatOpenAI(modelgpt-3.5-turbo, temperature0) # 创建检索链 qa_chain RetrievalQA.from_chain_type( llmllm, chain_typestuff, # 最常用的方式将所有检索到的文档“塞”进上下文 retrievervectorstore.as_retriever(search_kwargs{k: 4}), # 检索最相关的4个块 return_source_documentsTrue, # 返回来源便于调试 chain_type_kwargs{ prompt: PROMPT # 可以传入自定义的提示模板这是上下文工程的关键 } ) # 自定义提示模板示例 from langchain.prompts import PromptTemplate template 你是一个技术文档助手请严格根据以下提供的上下文信息来回答问题。如果上下文信息不足以回答问题请直接说“根据现有信息无法回答”不要编造信息。 上下文 {context} 问题{question} 请给出专业、准确的回答 PROMPT PromptTemplate( templatetemplate, input_variables[context, question] )核心解析chain_typestuff这是最简单的方式但受限于模型的上下文窗口。如果检索到的4个块总长度超过窗口就会失败。search_kwargs{k: 4}k值是需要调优的。太小可能信息不全太大可能引入噪声且成本高。可以从3开始根据回答质量调整。提示模板是灵魂{context}和{question}是占位符。清晰的指令“严格根据上下文”能极大减少幻觉。你可以加入角色设定、输出格式要求等。步骤2引入上下文压缩当检索到的文档块很多或很长时使用压缩。from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import LLMChainExtractor # 创建一个基于LLM的提取式压缩器 compressor LLMChainExtractor.from_llm(llm) # 包装原有的检索器 compression_retriever ContextualCompressionRetriever( base_compressorcompressor, base_retrievervectorstore.as_retriever(search_kwargs{k: 6}) # 可以检索更多让压缩器去筛选 ) # 现在使用压缩检索器来创建QA链 compressed_qa_chain RetrievalQA.from_chain_type( llmllm, chain_typestuff, retrievercompression_retriever, # 使用压缩检索器 return_source_documentsTrue, chain_type_kwargs{prompt: PROMPT} )工作原理LLMChainExtractor会先检索出6个块然后针对用户的具体问题要求LLM从这6个块中提取出与问题直接相关的句子或片段最终只将这些精华部分放入上下文。这相当于一个动态的、基于查询的摘要过程。4.3 第三阶段高级策略——对话历史管理对于多轮问答需要管理历史上下文。from langchain.memory import ConversationBufferWindowMemory, ConversationSummaryMemory # 方案A滑动窗口记忆 window_memory ConversationBufferWindowMemory( k5, # 只保留最近5轮对话 memory_keychat_history, return_messagesTrue ) # 方案B摘要记忆适合较长对话 summary_memory ConversationSummaryMemory( llmllm, memory_keychat_history, return_messagesTrue ) # 创建带记忆的链这里以ConversationalRetrievalChain为例 from langchain.chains import ConversationalRetrievalChain conversational_chain ConversationalRetrievalChain.from_llm( llmllm, retrievervectorstore.as_retriever(), memorywindow_memory, # 注入记忆 combine_docs_chain_kwargs{prompt: PROMPT} ) # 使用方式 result conversational_chain.invoke({question: 这个软件如何安装}) print(result[answer]) # 后续提问可以指代上文 result2 conversational_chain.invoke({question: 那它的配置文件在哪里}) # 模型知道“它”指代上个问题中的软件策略选择ConversationBufferWindowMemory简单高效但会完全遗忘k轮之前的对话。ConversationSummaryMemory每轮对话后用LLM生成一个不断更新的对话摘要。将摘要而非完整历史放入上下文极大节省Token。缺点是摘要可能丢失细节且每次更新都有额外LLM调用成本。混合策略一个更精细的策略是保留最近2-3轮完整对话同时对更早的对话进行摘要。这需要自定义记忆类来实现。5. 避坑指南与效能优化实战记录在实际部署中你会遇到各种预料之外的问题。以下是我从多个项目中总结出的核心经验。5.1 检索质量不佳根源往往是数据预处理问题现象用户问了一个明明文档里有的问题但系统检索到的都是不相关的片段导致回答错误或“无法回答”。排查清单与解决方案问题可能根源排查方法解决方案与优化技巧分割策略不当检查检索到的源文档块内容。是否一个完整的概念被切成了两半或者一个块里包含了多个不相关的主题调整chunk_size和chunk_overlap。尝试语义分割器。对于结构化文档API文档、手册优先按标题(#,##)分割。嵌入模型不匹配测试嵌入模型在你的领域数据上的表现。用一些核心术语计算相似度看是否合理。为中文场景务必选择双语或中文嵌入模型。在业务数据上微调嵌入模型是终极方案但成本高。可以先尝试bge、m3e等优秀开源中文模型。查询未优化直接使用用户的原始问题如“我该怎么弄”进行检索过于模糊。实施查询重写/扩展。在检索前用一个小模型如GPT-3.5将用户问题重写为更完整、包含关键实体的陈述句。例如“我该怎么弄” - “如何安装并配置[软件名X]”。元数据未利用检索时没有利用文档的元数据如所属章节、文件类型进行过滤。在分割时为每个块添加丰富的元数据如source:chapter_3.md,header:安装步骤。检索时让向量数据库支持元数据过滤例如只检索“安装”章节的块。相似度度量问题默认的余弦相似度在某些情况下可能不是最佳选择。对于某些嵌入模型尝试点积dot product或欧氏距离。但大多数情况下使用嵌入模型推荐的度量方式bge模型推荐余弦相似度且向量需归一化。一个关键技巧人工评估检索结果。随机抽取20-30个用户可能问的问题手动运行检索器查看返回的top_k个文档块是否相关。计算一个简单的检索命中率。这是优化检索环节最直接有效的方法。5.2 上下文过长与成本失控问题现象API调用费用增长过快或模型开始忽略上下文中间部分的信息长上下文衰减。优化策略设置硬性截断在将最终上下文提交给LLM前计算总Token数。如果超过阈值如模型最大限制的80%则启动压缩或优先级丢弃策略。from langchain.text_splitter import TokenTextSplitter token_splitter TokenTextSplitter(chunk_size8000, chunk_overlap200) # 按Token数计算更准 # 在组装上下文后进行检查和截断实施分层检索不要一次性检索所有内容。先检索最相关的少量文档如top-2如果LLM判断信息不足可以通过特定提示词让其输出“需要更多信息”的信号再发起第二轮检索。这被称为“递归检索”。拥抱更小的模型对于上下文压缩、查询重写、摘要生成等辅助任务完全可以使用更小、更便宜的模型如gpt-3.5-turbo甚至开源的7B/13B模型把最核心的答案生成任务留给能力强的大模型。这种“大小模型协同”的架构是控制成本的趋势。监控与告警为你的应用添加Token使用监控。记录每次调用的输入/输出Token数设置每日/每周预算告警。LangChain和LlamaIndex都提供了相关的回调Callback功能。5.3 模型幻觉与答案不准即使检索到了正确文档模型也可能“视而不见”或自行编造。对抗策略强化指令在系统提示词中反复强调“严格依据上下文”、“引用原文”。可以使用更强烈的措辞例如“你必须仅使用以下上下文中的信息。上下文之外的信息即使你知道也绝对不可以用来回答。”要求引用在提示词中要求模型在回答时指出依据来自上下文的哪个部分例如用【原文1】、【原文2】标注。这不仅能提高可信度也便于你事后验证和调试。后处理验证对于关键事实如日期、数字、名称可以设计一个后处理步骤。用检索到的原文去验证模型答案中的这些实体是否一致。如果不一致可以触发一次重新生成或直接给出“信息可能存在冲突”的提示。Few-Shot示例在上下文中提供1-2个“标准答案”示例。示例中清晰展示如何基于上下文片段进行回答以及当上下文不足时如何回应。这对引导模型遵循既定格式和规则非常有效。5.4 系统延迟与响应缓慢瓶颈分析与优化向量检索延迟如果向量数据库部署在海外或网络不佳检索可能成为瓶颈。方案将向量数据库部署在应用同一区域或使用云服务的全球加速网络。对于本地部署确保有足够的内存和索引优化。嵌入模型延迟如果每次查询重写或压缩都调用远程嵌入API延迟累加会很可观。方案将嵌入模型本地化部署。像BAAI/bge-small-zh这样的模型在CPU上推理一个句子也仅需几十毫秒。LLM调用延迟这是主要延迟来源。方案使用流式输出Streaming让用户尽快看到首个Token。对于非实时场景采用异步调用。考虑为不那么关键的任务设置更短的超时时间。架构层面的思考对于超大规模或对延迟极其敏感的应用可以考虑将最热门的知识如FAQ的问答结果预生成并缓存起来直接绕过检索和LLM生成步骤。这本质上是将动态的RAG系统与静态的缓存系统相结合。构建一个高效的上下文工程系统是一个持续迭代和调优的过程。从“Awesome-Context-Engineering”这样的资源库入手能让你快速站在巨人的肩膀上了解全貌。但真正的知识来自于将这些工具和理念应用于你自己的具体场景不断地测量、分析、调整。记住没有银弹最适合你业务的那套上下文策略一定是在理解了基本原理后通过大量实验摸索出来的。