高级链模式
📜 MapReduceChain 是如何处理超长文本的?它分为哪两个阶段?¶
MapReduceChain 是 LangChain 中用于处理超长文本(超出 LLM 上下文窗口限制)的一种文档合并与问答链。核心思想借鉴了大数据领域的 MapReduce 计算模型——先将大问题拆成小问题并行解决(Map),再汇总结果(Reduce)。
流程图示¶

Map 阶段¶
-
将输入的全部文档或超长文本按照 LLM 的
max_tokens限制自动分割成多个小块(chunks),保证每个 chunk 的 token 数不超出模型窗口。 -
对每个 chunk 独立且并行地调用 LLM,使用用户指定的 Map Prompt。典型的 Prompt 是:"请对以下文本进行总结:{text}"。Map 阶段的输出是各 chunk 的局部摘要或答案片段。
Reduce 阶段¶
-
将所有 Map 阶段输出的局部结果拼接在一起,作为 Reduce Prompt 的输入。
-
再次调用 LLM 一次,使用用户指定的 Reduce Prompt。典型的 Prompt 是:"以下是对不同部分内容的总结,请综合这些信息,给出一个完整的最终答案:{summaries}"。
-
如果 Reduce 阶段的输入本身仍然超长,LangChain 支持递归 Reduce,即对局部结果也进行 MapReduce,直到能在一个窗口内完成。
设计要点¶
-
并发控制:Map 阶段可以充分利用并发来加速处理,尤其是对于大批量文档。
-
信息损失:Map 阶段各自独立,可能导致跨块的上下文关联丢失。因此生成的摘要可能遗漏重要的全局关系。
-
适用场景:适合对大量独立的文档集合做摘要、多文档问答、以及需要并行提速的场景。
⚙️ 在一个 MapReduceChain 中,Map 阶段和 Reduce 阶段分别调用 LLM 多少次?如何控制并发?¶
调用次数¶
-
Map 阶段:调用 LLM 的次数 等于分割出的 chunk 数量。如果文档被切分成 10 个块,Map 阶段就会执行 10 次 LLM 调用。
-
Reduce 阶段:至少调用 LLM 1 次。如果所有 Map 输出拼接后仍在 token 限制内,则一次调用即可。否则,若 Reduce 输入超出窗口,LangChain 会启用递归 Reduce,此时 Reduce 阶段可能会调用多次 LLM,形成多级合并。
并发控制¶
LangChain 使用 Python 的 asyncio 或 ThreadPoolExecutor 来实现并发。通过以下参数控制:
-
concurrency:在创建 MapReduceChain 时,设置LLMChain的并发级别。默认不限制,但可指定最大并发数来避免 API 限流或资源过载。 -
对于
ChatOpenAI等模型,可以在初始化时设置max_concurrency来限制同时发出的请求数。 -
如果要精细控制,可以在调用链的
arun方法(异步)中结合asyncio.Semaphore自定义并发。
代码示例¶
from langchain.chains import MapReduceDocumentsChain, ReduceDocumentsChain, StuffDocumentsChain
from langchain.chains.llm import LLMChain
from langchain.prompts import PromptTemplate
from langchain.chat_models import ChatOpenAI
# 1. Map 链
map_template = "请总结以下内容:\n{text}\n总结:"
map_prompt = PromptTemplate(template=map_template, input_variables=["text"])
map_chain = LLMChain(llm=ChatOpenAI(model="gpt-4", max_concurrency=10), prompt=map_prompt)
# 2. Reduce 链
reduce_template = "以下是对不同部分的总结:\n{text}\n综合以上,给出一个连贯的摘要:"
reduce_prompt = PromptTemplate(template=reduce_template, input_variables=["text"])
reduce_chain = LLMChain(llm=ChatOpenAI(model="gpt-4"), prompt=reduce_prompt)
combine_documents_chain = StuffDocumentsChain(llm_chain=reduce_chain, document_variable_name="text")
# 3. 组合成 MapReduce
reduce_documents_chain = ReduceDocumentsChain(
combine_documents_chain=combine_documents_chain,
collapse_documents_chain=combine_documents_chain, # 递归reduce
token_max=4000,
)
map_reduce_chain = MapReduceDocumentsChain(
llm_chain=map_chain,
reduce_documents_chain=reduce_documents_chain,
document_variable_name="text",
)
🌀 与 MapReduce 相比,RefineChain 的迭代式摘要有什么优缺点?¶
RefineChain 是一种顺序处理的摘要策略。它不是先并行处理所有块再合并,而是像“滚雪球”一样,每次读入一个新块,与前面已积累的摘要一起输入 LLM,逐步精炼成一个最终摘要。
流程¶
优点¶
-
上下文连贯性强:每次精炼时,LLM 都能看到之前所有内容的摘要,因此能更好地捕捉文档的全局结构、故事线或逻辑链,不会因分块而丢失重要关联。
-
信息融合深度高:迭代式地不断更新摘要,允许模型进行更复杂的推理和综合,往往能生成质量更高的摘要。
-
适用于结构化文本:对于小说、论文等有内在逻辑连贯性的内容,Refine 能更好地保留情节和因果关系。
缺点¶
-
速度慢、无法并行:必须顺序处理每个块,总耗时等于所有块的处理时间之和,无法利用并发加速。对于大量文档来说极其缓慢。
-
API 调用次数多:假设有 N 个块,则需调用约 N 次(第一次生成初始摘要,后续每个块一次 Refine),与 MapReduce 的 (N+1) 次相当,但无法并行。
-
误差累积风险:如果早期块的摘要出现偏差,这个错误可能会在后续迭代中被放大,因为每次精炼都是基于之前的摘要。
对比 MapReduce¶
图标:Refine 像流水线:Chunk1 -> Summary1 -> +Chunk2 -> Summary2 -> ... -> Final Summary
MapReduce:Chunk1,Chunk2,... -> 并行 Summaries -> Merge -> Final
📖 当处理一本长篇小说的摘要时,你推荐用 MapReduce 还是 Refine?为什么?¶
强烈推荐 RefineChain。
理由¶
-
小说具有极强的叙事连贯性:人物、情节、伏笔、情感线索贯穿全文,不可割裂。MapReduce 的并行处理导致各块摘要独立生成,必然丢失这些关键关联。最终 Reduce 拼接成的摘要很可能像一堆情节片段的罗列,读起来支离破碎,丢失原著的文学魅力。
-
Refine 能够模拟“阅读-积累-更新”的认知过程:模型像一个读者,先读开头得到初始印象,再读后续章节,不断修正和丰富对整个故事的理解。这种方式产生的摘要能保留故事主线、人物发展和主题。
-
质量优先于速度:对于单篇小说摘要,用户通常愿意等待更长时间来换取高质量的阅读体验。串行处理虽然在速度上吃亏,但最终摘要的质量远胜 MapReduce。
如果有性能需求怎么办?¶
如果小说极长(比如上百万字),且必须在短时间内完成,可以考虑改良的混合策略:
-
先将小说按章节(自然分割点)切分,对每个章节独立使用 Refine 生成章节摘要。
-
然后将这些章节摘要用 MapReduce 合并为整篇摘要。
-
这样既能利用 Refine 保留章节内连贯性,又能借助 MapReduce 的并行性加速。但要注意,跨章信息仍可能丢失。
🗣️ 解释 ConversationalRetrievalChain 的工作原理:它如何将对话历史、新问题和检索到的文档组合?¶
ConversationalRetrievalChain 是 LangChain 中实现多轮对话式检索问答的核心链。它解决了普通检索问答不能记忆对话上下文的问题。
工作流程¶

三部分组合逻辑¶
-
对话历史:原始的用户-助手多轮对话文本。它保留在最终 QA 的 Prompt 中,让 LLM 知道讨论的来龙去脉,以便生成连贯的回答。
-
新问题:当前用户的最新提问。它首先被用于生成检索查询,然后在最终 QA 中结合历史和文档给出答案。
-
检索到的文档:由 Retriever 根据独立查询从知识库中搜索到的相关文本片段。它们作为背景知识注入 Prompt,使 LLM 能够基于事实给出有根据的回答。
关键 Prompt 示例¶
通过这个“重写查询”的步骤,模型会将指代(“他”、“那个”、“上次说的”)消解为具体的实体,并将上下文融入查询中,从而极大地提高检索的准确性。
图示:
[History+Question] → (LLM) → Refined Query → Retriever → Docs
↓
[History+Question+Docs] → (LLM) → Final Answer
🔎 ConversationalRetrievalChain 中的“condense_question_prompt”是用来做什么的?为什么需要它?¶
condense_question_prompt 是 ConversationalRetrievalChain 中一个至关重要的环节,它的任务是将用户的原始问题与对话历史融合,重写为一个独立、完整、适合检索的查询。
为什么需要它?¶
在多轮对话中,用户的问题往往充满省略和指代。比如:
-
用户第一问:“帮我找一下关于量子计算的资料”
-
助手回答:“量子计算是利用量子比特进行计算的技术…”
-
用户第二问:“它有什么应用?”
这里的“它”指的是“量子计算”。如果直接用“它有什么应用?”去向量数据库检索,结果会完全错误,因为向量模型无法从孤立句子中理解上下文。condense_question_prompt 的作用就是将“它有什么应用?”重写为“量子计算有什么应用?”,进而实现精准检索。
具体流程¶
-
LLM 被调用,传入对话历史 + 用户新问题。
-
Prompt 模板示例:
-
LLM 输出重写后的问题,例如:“量子计算有什么应用?”
-
这个独立问题被发送给 Retriever,从而检索到相关文档。
额外价值¶
-
提取关键实体:LLM 能识别出历史中提到的关键概念,并将其补全到新问题中。
-
统一查询风格:确保检索器的输入始终是清晰、完整的自然语言问题,充分发挥向量检索的语义理解能力。
-
减少对话历史噪音:对话历史可能很长且包含无关信息,重写查询相当于自动做了关键信息抽取和问题重构。
🧩 如果你想在 ConversationalRetrievalChain 中同时使用多个检索器(如向量库 + 传统搜索),如何设计?¶
要实现多检索器混合,核心思路是并行检索、合并去重、统一排序。LangChain 本身支持 EnsembleRetriever 组合多个检索器,但也可以自定义更复杂的融合逻辑。
设计方案¶
- 初始化多个检索器
vector_retriever:从向量数据库(如 Chroma、Pinecone)基于语义相似度检索。keyword_retriever:基于 BM25 等传统词频算法,或直接使用 Elasticsearch 全文搜索。-
可选:
web_search_retriever:调用 SerpAPI 等进行网络搜索。 -
组合检索器
- 使用 LangChain 的
EnsembleRetriever将多个检索器打包,它采用加权 Reciprocal Rank Fusion (RRF) 算法对来自不同检索器的结果统一排序,合并成最终文档列表。 - 示例:
from langchain.retrievers import EnsembleRetriever
ensemble_retriever = EnsembleRetriever(
retrievers=[vector_retriever, keyword_retriever],
weights=[0.7, 0.3] # 语义检索权重更高
)
- 嵌入 ConversationalRetrievalChain
- 将
ensemble_retriever直接作为retriever参数传入ConversationalRetrievalChain.from_llm()。 - 原链的
condense_question_prompt生成独立查询后,会交给ensemble_retriever进行多路检索,获得合并排序后的文档。 - 后续的问答 Prompt 不变,仍使用对话历史、新问题和这些文档生成最终答案。
高级定制:分阶段检索¶
如果希望更精细控制,比如先关键词检索缩小范围,再语义检索精排,或者对某些领域使用专用检索器,可以自定义一个 BaseRetriever 子类,在其中实现调度逻辑。
设计注意事项¶
-
去重:相同内容可能来自不同检索器,需要去除重复文档(EnsembleRetriever 默认处理)。
-
延迟:多路检索增加了耗时,可考虑异步并发调用两个检索器,然后等待所有结果返回后合并。
-
排序加权:根据实际业务调整各检索器的权重,必要时可通过评估集验证。

📂 RetrievalQA 链有哪几种 chain_type?(stuff, map_reduce, refine, map_rerank)各自的特点?¶
RetrievalQA 链是 LangChain 中实现检索增强生成(RAG)的经典链,它支持四种 chain_type,对应不同的文档组合策略。选择哪个取决于文档数量和 token 限制。
四种链类型对比¶
详细特点¶
-
stuff:就像一个“全塞进去”的口袋。Prompt 里包含所有检索到的文档和用户问题。LLM 可以一次看到所有信息,因此能最好地捕捉文档间的联系。但一旦文档总 token 超限,就无法使用。
-
map_reduce:前面已详述。关键在于并行,但跨块信息容易丢失。
-
refine:前面已详述。串行精炼,质量高但慢。
-
map_rerank:这是一个特殊的链。它对每个检索文档调用 LLM,要求 LLM 不仅回答问题,还要输出一个置信度分数(如 0-100)。然后选择分数最高的那份回答作为最终答案。它不进行文档间的信息融合,只是“挑一个最好的”。
如何选择?¶
-
如果检索出的文档很少(<3 个片段,总 token < 窗口)→ stuff。
-
如果文档非常多,且相对独立,对速度有要求 → map_reduce。
-
如果文档有内在连贯性(如长文、书籍),追求高质量摘要/答案 → refine。
-
如果只是想从一堆候选文章中找最相关的单一答案,且希望模型给出自信度 → map_rerank。
图例:
stuff: [Doc1, Doc2, Doc3] + Question → LLM → Answer
map_reduce: [Doc1, Doc2, Doc3] → (Map) → [Partial Answers] → (Reduce) → Final Answer
refine: Doc1 → Ans1 → +Doc2 → Ans2 → +Doc3 → Final Answer
map_rerank: [Doc1, Doc2, Doc3] → 各自评分 → 选最高分的答案
💣 stuff 模式下,如果检索到的文档总长度超过模型最大 token 限制,会怎样?如何解决?¶
💥 会发生什么?
在 stuff 链中,LangChain 会试图将所有检索到的文档拼接到一个 Prompt 中。如果合并后的文本 token 数超过了 LLM 的最大上下文窗口(例如 gpt-3.5-turbo 的 4096 tokens),会直接触发 API 报错(InvalidRequestError: maximum context length exceeded)。如果模型是本地部署的,可能出现截断、生成乱码或忽略超出部分。
🛠️ 解决方案
-
方案一:截断文档(Truncation) 最简单粗暴。在拼接前,根据剩余的 token 预算,从第一个文档开始逐个追加,当达到上限时丢弃后续所有文档。LangChain 的
StuffDocumentsChain不支持自动截断,需要自定义预处理,计算 token 数并截断。但会丢失关键信息。 -
方案二:切换到其他链类型 改用
map_reduce或refine链。它们专为超长文档设计,通过分块并行处理或迭代精炼来突破窗口限制。这是最推荐的工程方案。 -
方案三:文档过滤与重排序 在检索阶段就严格控制返回的文档数量。使用
Retriever的k参数限制返回 top-k,并结合SimilarityScoreThreshold只保留相关性高于阈值的文档。也可以先用一个轻量级模型(如 BM25)粗筛,再用向量检索精排。 -
方案四:动态 Token 预算管理 自己实现一个
DocumentCompressor,在链执行前计算 token 数,如果超限,则使用 LLM 对每个文档进行压缩(例如“请用一句话总结该文档”),或使用ContextualCompressionRetriever对文档进行有损压缩,直到总长度合适。 -
方案五:智能切分与滑动窗口 将超长文本按句子或段落切块,然后用
map_rerank对每个块打分,只保留得分最高的几个块拼接回答。
💡 实战建议:永远不要在生产环境依赖 stuff 处理不确定数量的文档。应在检索阶段就做好长度控制,或默认使用 refine/map_reduce 并设置 token_max 参数。
🎯 map_rerank 模式中,如何对文档进行评分和重排序?内部如何调用 LLM?¶
🔍 工作原理
map_rerank 链会对每一个检索到的文档独立调用 LLM,要求 LLM 不仅回答问题,还输出一个数值化的置信度/相关性评分。然后根据评分对文档排序,选择得分最高的文档对应的答案,或者再进一步处理。
🔧 内部调用流程
-
对于每个文档
doc,构造一个特殊的 Prompt 模板,该模板必须要求 LLM 在回答后附加一个明确的评分标记,例如Score: 85/100。 -
LangChain 使用
MapRerankDocumentsChain,其内部会调用LLMChain逐文档执行。 -
解析 LLM 输出:从回答中提取最后的数字作为该文档的得分。如果解析失败,该文档得分设为 0。
-
所有文档处理完后,按得分降序排列。
-
选择得分最高的文档,返回其对应的答案。
📝 关键 Prompt 设计
一个典型的 map_rerank prompt 如下:
LLM 的输出可能类似:这个文档讲述了...因此答案是... Score: 92。LangChain 默认使用正则 r"Score:\s*(\d+)" 提取评分。
💡 优点与局限
-
优点:可以快速从多个文档中定位最相关的一个,适合单一答案场景。
-
局限:
- 每个文档需调用一次 LLM,文档多时成本高。
- 评分完全依赖 LLM 的判断,可能不稳定。
- 只保留一个文档的答案,无法综合多文档信息。
- 不适合需要综合多个文档才能回答的问题。
🛠️ 自定义增强
你可以修改评分提取逻辑,或者使用 MapRerankDocumentsChain 的 callback 记录每个文档的得分用于后续分析。如果希望最终答案也参考其他高分文档,可以在排序后取前 2-3 个文档,用 stuff 链再回答一次。
🔁 在 ConversationalRetrievalChain 中,如果用户连续提问,如何把历史问题和回答作为新检索的上下文?¶
ConversationalRetrievalChain 已经内置了将对话历史融入检索查询的机制——condense_question_prompt。这是解决“指代消解”和“上下文补充”的核心。
🔄 其工作方式为:
-
当用户提出新问题
question时,链不会直接将question发送给检索器。 -
它首先调用 LLM,使用
condense_question_prompt将对话历史 +question重写为一个独立的、自包含的检索查询。 -
然后,用这个重写后的查询去检索文档。
-
最后,将原始对话历史、
question和检索到的文档一并送入最终的回答链。
📝 默认 Prompt 剖析
LLM 会自动把 它、他、那个 等代词替换为历史中提到的实体,并补充省略的约束条件。
🚀 如果要更深度地利用历史
你完全可以自定义这个 Prompt。例如,不仅消解指代,还提取历史中的关键约束(“我要便宜的”),或者将历史回答作为新的检索条件(“结合上次推荐的餐厅,再找类似风格的”)。只需修改 condense_question_prompt 模板即可。
代码示例(自定义更积极的查询重写):
custom_condense_prompt = PromptTemplate.from_template(
"根据以下对话历史,将用户的后续问题重写为一个详细的搜索查询,包含历史中提到的所有关键信息(如人名、地点、偏好等)。"
"\n\n对话历史:\n{chat_history}\n\n后续问题: {question}\n\n详细的搜索查询:"
)
qa = ConversationalRetrievalChain.from_llm(
llm,
retriever,
condense_question_prompt=custom_condense_prompt,
...
)
这样检索时就能携带更多上下文,提高召回精度。
🎯 写一段代码,实现一个自定义的“文档打分+过滤”链,只保留最相关的 3 个文档片段给 LLM。¶
这里我们实现一个自定义链,它结合了 MapRerank 的思想,但并非直接从 LangChain 继承,而是用函数式风格组装,便于理解。我们将使用一个评分 Prompt 让 LLM 给每个文档打分,然后排序取前三。
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain.chat_models import ChatOpenAI
from langchain.schema import Document
import re
# 1. 定义评分 Prompt
score_prompt = PromptTemplate.from_template(
"""你是一个专业的文档相关性评估专家。请根据用户的问题,评估以下文档片段的相关性,并给出一个1-100的整数分数。
用户问题: {question}
文档片段: {document}
请直接输出分数,格式为"Score: 数字",不要输出其他内容。"""
)
# 2. 创建LLM链
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
score_chain = LLMChain(llm=llm, prompt=score_prompt)
# 3. 打分函数,处理单个文档,返回 (doc, score)
def score_document(question: str, doc: Document) -> (Document, int):
raw = score_chain.run(question=question, document=doc.page_content)
match = re.search(r"Score:\s*(\d+)", raw)
if match:
score = int(match.group(1))
else:
score = 0
return doc, score
# 4. 主过滤函数:传入文档列表和问题,返回top_k文档
def filter_top_k(question: str, docs: list[Document], k: int = 3) -> list[Document]:
scored = []
for doc in docs:
_, score = score_document(question, doc)
scored.append((doc, score))
# 按得分降序排序
scored.sort(key=lambda x: x[1], reverse=True)
top_docs = [doc for doc, _ in scored[:k]]
return top_docs
# 5. 使用示例
# 假设 docs 是从 Retriever 获取的候选文档
question = "什么是量子纠缠?"
top3_docs = filter_top_k(question, docs, k=3)
# 之后可以将 top3_docs 交给 stuff 或其他链生成最终答案
⚠️ 注意事项:
-
此实现每次打分调用一次 LLM,文档多时成本高。可改用更便宜的模型(如 gpt-3.5-turbo)或批量打分。
-
如果文档片段较长,需要截断,避免超出打分模型的 token 限制。
-
对于生产环境,建议使用异步并发(
asyncio.gather)同时为多个文档打分,加速处理。
⚡ 你如何对 RetrievalQA 链进行性能优化?(如缓存检索结果、缩短 prompt)¶
优化 RetrievalQA 链主要从检索速度、LLM 调用成本、响应延迟三个方面着手。
-
缓存检索结果
-
LangChain 内置缓存:
from langchain.cache import InMemoryCache,设置langchain.llm_cache = InMemoryCache(),可以缓存 LLM 的调用结果。对于相同的问题和文档组合,直接返回缓存答案。 -
自定义缓存:使用 Redis 或磁盘缓存检索到的文档。在调用链之前,先检查缓存中是否有该问题的文档列表,避免重复检索。
-
多级缓存:先查内存缓存,再查向量数据库缓存(某些向量库支持查询结果缓存)。
-
缩短 Prompt 和减少 Token 消耗
-
精简 Prompt 模板:去除不必要的废话,用简洁的指令。例如,不要写“请你作为一名专家…”,直接“根据以下文档回答问题”。
-
文档压缩:使用
ContextualCompressionRetriever加LLMChainExtractor,在文档进入 Prompt 前,用 LLM 提取出与问题最相关的句子,丢弃冗余。 -
减少检索文档数量:调小
k值,或使用相似度阈值过滤掉低相关文档。 -
只返回答案,不要求解释:在 Prompt 中明确指示只输出答案。
-
加速检索
-
混合检索:先用 BM25 等快速算法粗筛,再用向量检索精排,减少向量检索的候选集。
-
离线索引优化:使用 FAISS 的
IndexIVFPQ等压缩索引,加速相似度搜索。 -
异步并行:对于
map_reduce或map_rerank,利用asyncio并发调用 LLM。 -
降低 LLM 调用次数
-
优先使用
stuff链(如果文档很短),它只调用一次 LLM。 -
对于
refine,如果中间结果已经很完善,可以提前终止。 -
使用更小、更快的模型进行初次过滤(如用 embedding 模型对文档打分),再交给大模型回答。
-
连接池与批处理
-
使用
HttpClient的持久连接,避免每次请求重建连接。 -
对于
map_reduce,可以将 Map 任务批量发送,利用 API 的批量接口(如果支持)。 -
模型侧优化
-
使用量化后的本地模型(如 llama.cpp)降低推理延迟。
-
对于重复问题,设置
temperature=0并缓存结果。
🛠️ 一个综合缓存示例:
from langchain.cache import RedisCache
import redis
redis_client = redis.Redis(host='localhost', port=6379)
langchain.llm_cache = RedisCache(redis_client)
# 同时对检索器使用磁盘缓存(需自定义)
class CachedRetriever(BaseRetriever):
def _get_relevant_documents(self, query):
if query in self.cache:
return self.cache[query]
docs = self.base_retriever.get_relevant_documents(query)
self.cache[query] = docs
return docs
结合多种策略,可以将 RetrievalQA 的端到端延迟降低 50% 以上。
🕸️ 什么是“多跳问题”(Multi-hop Question)?LangChain 中有哪些链专门针对多跳问题优化?¶
🕸️ 多跳问题 是指需要综合多个不同信息源,经过多步推理才能回答的问题。例如:“《哈利波特》中,哈利的教父小天狼星布莱克,他的堂姐的女儿是谁?” 回答需要:1)找到小天狼星布莱克;2)找到他的堂姐贝拉特里克斯;3)找到贝拉特里克斯的女儿戴尔菲。这需要多次检索和逻辑跳转。
LangChain 中的优化链
LangChain 本身没有一个叫“MultiHopChain”的单一类,但提供了一些组合链和工具,可以构建多跳推理流程:
-
MultiPromptChain/RouterChain:将复杂问题路由到不同的专业链。例如,一个链负责提取实体,一个链负责查询关系。 -
SequentialChain:自定义多个步骤的链。如 Step1:提取问题中的实体;Step2:搜索该实体;Step3:从结果中提取下一个实体;Step4:再次搜索。 -
ConversationalRetrievalChain:结合历史进行检索,适合对话式多跳。 -
ReAct模式:使用Agent+Tools,让 LLM 自主决定何时检索、检索什么,并观察结果后决定下一步。这是目前处理复杂推理最灵活的方式。 -
Self-Ask with Search:一种特殊的 Agent,明确要求 LLM 分解问题为子问题,并对每个子问题执行搜索,最后合并答案。LangChain 中通过create_self_ask_with_search_agent实现。
💡 实战选择:
-
如果问题可预测,用
SequentialChain硬编码流程。 -
如果问题多变,用
ReAct Agent挂接检索工具,让 LLM 自己规划跳数。这是最通用、最强大的多跳解决方案。 -
也可以结合
GraphCypherQAChain(如果数据在知识图谱中),天然支持多跳关系查询。
🔧 如何自定义一个支持工具调用的检索链?即 LLM 可以根据需要主动调用搜索工具。¶
这本质上是创建一个 Agent,而不是普通的 Chain。Agent 的核心是让 LLM 决定何时、用何参数调用哪个工具。
实现步骤:
- 定义工具:将你的检索器封装为
Tool对象。
from langchain.tools import Tool
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings
vectorstore = Chroma(...)
retriever = vectorstore.as_retriever()
def retrieve_docs(query: str) -> str:
docs = retriever.get_relevant_documents(query)
return "\n\n".join([d.page_content for d in docs])
search_tool = Tool(
name="知识库检索",
func=retrieve_docs,
description="当需要查找特定信息、事实或概念时使用此工具。输入应为自然语言查询。"
)
- 创建 Agent:使用
create_openai_functions_agent或create_react_agent,将工具列表传入。
from langchain.agents import create_openai_functions_agent, AgentExecutor
from langchain import hub
prompt = hub.pull("hwchase17/openai-functions-agent")
llm = ChatOpenAI(model="gpt-4", temperature=0)
agent = create_openai_functions_agent(llm, [search_tool], prompt)
agent_executor = AgentExecutor(agent=agent, tools=[search_tool], verbose=True)
-
运行:Agent 会自动处理用户输入,如果需要,它会生成一个包含工具调用的消息,LangChain 拦截执行你的
retrieve_docs函数,并将结果返回给 LLM 继续生成最终答案。 -
自定义工具调用逻辑:如果你需要在检索前后加入额外处理(如过滤、重排序),可以在工具函数内部实现,或者使用
Tool的coroutine实现异步。你也可以限制 LLM 的调用次数(max_iterations),防止无休止调用。
🚀 高级用法:可以构建多工具协作,如一个工具返回文档列表,另一个工具返回文档摘要,让 Agent 自己选择用哪个。
🧠 LangChain 中“Chain of Thought” (CoT) 一般如何通过 Prompt 实现?有没有现成的链?¶
CoT 的本质是通过在 Prompt 中明确要求 LLM 展示推理步骤,从而激发其复杂推理能力。LangChain 中并没有一个叫 CoTChain 的单独类,而是通过Prompt 模板和Agent 模式来实现。
📝 通过 Prompt 实现 CoT 的几种方式:
- 零样本 CoT:在 Prompt 末尾加上
Let's think step by step。
-
少样本 CoT:在 Prompt 中提供几个带有完整推理步骤的示例。
-
结构化 CoT:要求 LLM 输出 JSON 格式,包含
thought和action字段,如 ReAct。
🔗 现成的链或组件:
-
LLMChain:你只需传入自定义的 CoT Prompt 即可。 -
create_react_agent:内建的 ReAct Agent,实现了“思考-行动-观察”循环,这是 CoT 的一种具体应用。 -
create_structured_chat_agent:支持多步推理和工具调用,Prompt 中强制要求输出Thought,Action,Observation等。 -
PlanAndExecute代理:先规划步骤,再执行,天然带有 CoT 属性。
💡 实战建议:如果只是简单的推理问题,直接写一个 LLMChain 配合 CoT Prompt。如果涉及工具调用,用 ReAct Agent。CoT 的 Prompt 设计至关重要,避免过于开放导致回答冗长。
💻 解释“LLMBashChain”这种高级链:它怎样让 LLM 写出并执行 bash 命令?¶
LLMBashChain 是一个实验性的、极具危险性的链,它允许 LLM 生成 Bash 命令并在你的系统上实际执行。它主要用于自动化服务器管理、数据分析等需要与操作系统交互的场景。
🔄 工作流程:
-
用户提出一个自然语言请求,如“列出当前目录下最大的5个文件”。
-
LLM 生成对应的 Bash 命令,例如
ls -lhS | tail -5。 -
该命令被提交到系统 Shell 执行。
-
执行结果(标准输出和错误)被捕获,返回给 LLM。
-
LLM 根据命令输出生成最终的自然语言回答,如“最大的5个文件是:...”。
⚠️ 安全警告:这极其危险,因为 LLM 可能生成破坏性命令(rm -rf /),或者被注入恶意指令。绝对不要在未隔离的环境中使用。LangChain 文档强烈建议在沙箱(Docker容器)中运行,并设置严格的权限。
🛠️ 内部实现简析:
LLMBashChain 内部使用了 BashProcess 工具(基于 subprocess),并通过 Agent 框架让 LLM 决定是否调用该工具。大致逻辑如下:
from langchain.tools import Tool
from langchain.utilities import BashProcess
bash = BashProcess()
bash_tool = Tool(name="Bash", func=bash.run, description="执行Bash命令并返回输出")
# 然后创建Agent,将bash_tool作为唯一工具。
LLM 生成的命令会经过验证(可选),然后执行。你可以通过 BashProcess 的参数限制可执行命令,或使用 return_err=True 捕获错误。
✅ 适用场景:
-
自动化运维(需严格审计)
-
数据分析流水线
-
教学演示
🛡️ 防护措施:
-
始终在 Docker 容器内运行。
-
限制可用的 Shell 命令(白名单)。
-
要求用户确认(
input("是否执行?(y/n)"))。 -
审计所有命令日志。
🔧 在你的实际项目中,哪个高级链使用频率最高?你踩过哪些坑?¶
在我参与过的几个项目中,ConversationalRetrievalChain 是使用频率最高的,几乎任何涉及“对话式知识库问答”的产品都会用到。
✨ 为什么它最常用?
-
它解决了真实应用中的两大痛点:记忆对话上下文和基于私有知识回答。
-
用户希望像与人聊天一样连续提问,并能深入探讨文档内容。
-
开箱即用,可定制性强。
💣 踩过的坑及解决方式:
坑1:检索质量随时间下降
-
现象:多轮对话后,
condense_question_prompt生成的重写查询越来越模糊,导致检索出无关文档。 -
原因:对话历史太长,LLM 在重写时丢失了早期关键信息,或产生了幻觉。
-
解决:
- 限制传入的
chat_history长度,只保留最近 3-4 轮对话。 - 自定义
condense_question_prompt,要求 LLM 优先提取最新的约束,并总结历史意图。 - 使用
ConversationSummaryBufferMemory,自动维护一个对话摘要,替代原始历史。
坑2:引用来源不准确
-
现象:模型回答看似合理,但指出的来源文档并不包含该信息。
-
原因:LLM 在生成时混淆了多个文档片段,或自己编造了引用。
-
解决:
- 在最终 Prompt 中强制要求:“如果你的回答基于某个文档,请引用[文档编号]”,并为每个文档编号。
- 在返回答案后,通过后处理验证来源是否包含答案中的关键实体。
- 使用
RetrievalQAWithSources链。
坑3:成本失控
-
现象:API 费用激增。
-
原因:每次对话都调用 LLM 重写查询 + 最终问答,而历史很长时 token 消耗巨大。
-
解决:
- 对常见问题设置缓存。
- 使用更便宜的模型(gpt-3.5-turbo)进行查询重写和文档过滤,只在最终回答使用高级模型。
- 监控每个会话的 token 消耗并设置警报。
坑4:链的初始化耗时
-
现象:服务启动慢。
-
原因:
ConversationalRetrievalChain在初始化时需要加载多个 Prompt 和链。 -
解决:在应用启动时预加载,以单例模式复用。
⚖️ 如果要做一个“法律文书摘要”的链,你会选择哪种链模式?为什么?¶
法律文书(如判决书、起诉书)通常篇幅长、结构复杂、术语专业,且对信息的准确性和完整性要求极高。我会选择 混合链策略,核心是 Recursive RefineChain 结合自定义预处理。
📚 为什么不用 Stuff 或 MapReduce?
-
Stuff:法律文书轻易超过 token 限制,不可能。
-
MapReduce:会将文书割裂成独立片段并行摘要,必然丢失案件逻辑链条(原告-被告-争议焦点-判决推理),导致摘要碎片化,遗漏关键因果关系。
-
Refine:虽然能保留逻辑连贯性,但对于极长文书(几百页),Refine 的串行速度过慢,且随着迭代,模型可能会“遗忘”或过度压缩早期信息。
🛠️ 推荐方案:分段总结 + 分层提炼
-
智能切分:不按固定 token 切,而是利用法律文书的自然结构(按章、节、条款)或通过一个轻量级模型自动识别语义边界来切分。这样每个块保持相对完整的主题。
-
第一层:结构感知摘要:对每个块,使用定制 Prompt 进行摘要,要求提取特定要素(如“本段涉及当事人”、“核心争议”、“法律依据”),而不是泛泛总结。
-
第二层:分层融合:将第一层的要素摘要按逻辑分组(如将所有关于同一当事人的摘要合并),然后对这些组合使用
Refine链生成当事人相关部分的概要。 -
第三层:全局 Refine:将第二层的各部分概要,按照法律文书的逻辑顺序(例如:引言→事实→理由→判决)拼接,最后用一次
Refine(或直接用Stuff如果此时总长度允许)生成最终摘要,并要求 LLM 严格遵循输出格式。
💻 示例 Prompt 片段(第一层):
你是一名专业法律助手。请阅读以下法律文书片段,并提取以下信息:
- 涉及的当事人(原告、被告等)
- 核心法律争议点
- 引用的关键法条
- 判决结果(如有)
如果某条信息未出现,写“无”。
文本:{chunk}
这种要素抽取+分层汇总的方法,比单纯让 LLM 写连贯摘要更可控,也更容易后处理验证。
🌟 最终效果:生成的摘要既保留了全局逻辑,又能准确提炼法律要素,且由于分层处理,可以并行加速部分阶段。
🌍 你认为 LangChain 在高级链方面最大的贡献是什么?还存在哪些不足?¶
🚀 最大贡献
-
抽象与标准化了 LLM 应用的“设计模式” LangChain 将常见的 LLM 应用模式(如 RAG、对话记忆、工具使用、多步推理)抽象为可组合的链,使开发者可以从“怎么写 Prompt”迅速跃迁到“如何编排业务流程”。这种“乐高式”思想极大地加速了原型构建和社区知识共享。
-
提供了 Agent 与工具框架 通过
AgentExecutor和Tool接口,LangChain 让 LLM 能够真正与现实世界交互。ReAct 等模式的标准化,使得复杂推理任务的实现变得简单,是向自主智能体演进的重要一步。 -
打通了多模态和多后端 支持 OpenAI、HuggingFace、本地模型,以及向量数据库、文档加载器、嵌入模型的统一接口,降低了切换成本,促进了生态繁荣。
-
普及了“记忆”和“索引”的概念 对话记忆、摘要记忆、向量索引等组件,让开发者迅速理解如何为 LLM 注入长期和外部知识。
⚠️ 存在的不足
-
封装过度,抽象泄露严重 链的层级太深,参数传递不透明,调试极其困难。一个简单的错误往往需要追踪五六层调用栈。对于追求可控性的生产环境,过度的魔法让人不安。
-
性能开销与资源浪费 默认实现常常多次序列化/反序列化 Prompt,重复调用模型。在不需要复杂性的场景,LangChain 显得臃肿。
-
文档与代码不一致 更新频繁,文档滞后,许多示例已过时。且高级链的默认 Prompt 往往并非最优,容易被忽视。
-
对复杂多步推理的支持仍有限 虽然提供了 Agent,但长程规划、自我纠错、工具调用的可靠性仍是巨大挑战,容易出现无限循环或错误调用。
-
缺乏生产级特性 没有内建的缓存、限流、异步并发的最佳实践,错误处理和重试机制薄弱,需要大量自定义工作才能部署。
💡 总体评价:LangChain 是 LLM 应用开发从“手工作坊”走向“工业化”的里程碑,但它更适合快速实验和原型。在真正的生产级应用中,开发者应汲取其设计理念,但保留替换核心组件的自由。