第六章:LangChain与AutoGen框架实战
第六章:LangChain/AutoGen框架实战
6.1 LangChain核心组件与实战
6.1.1 LangChain架构概览与核心概念
先说一个真实的痛点。
2022年底,LangChain刚出来那会儿,如果你要让大模型"接入"自己的私有数据,基本上是手写一切:
- 自己写向量相似度计算
- 自己拼Prompt模板
- 自己管理对话历史
- 自己调用OpenAI API、处理重试、处理限流
- 自己解析Function Calling的输出
- 自己把工具执行结果塞回Prompt
一套下来,几百行代码,换个模型就得大面积改。更别提要接RAG、要接Agent、要换向量数据库……
LangChain要解决的就是这个问题——把大模型应用开发中那些"反复要写的脏活累活"抽象成标准组件,像搭积木一样拼出应用。
LangChain的核心设计哲学
LangChain的命名其实说清楚了它的定位:把大模型(Chain)和各种外部能力"链"起来。
它的架构围绕6个核心模块展开,这6个模块几乎覆盖了所有LLM应用的需求:
┌────────────────────────────────────────────────┐
│ LangChain 架构 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌────────┐│
│ │ Model I/O │ │ Prompts │ │ Chains ││
│ │ 模型交互 │ │ 提示词模板 │ │ 工作流 ││
│ └──────────┘ └──────────┘ └────────┘│
│ ↓ ↓ ↓ │
│ ┌──────────┐ ┌──────────┐ ┌────────┐│
│ │ Retrieval │ │ Agents │ │ Memory ││
│ │ 检索增强 │ │ 智能体 │ │ 记忆 ││
│ └──────────┘ └──────────┘ └────────┘│
└────────────────────────────────────────────────┘
用一句话理解:LangChain = 大模型应用的"标准库",就像Python标准库让你可以不依赖第三方包就写出功能完整的程序,LangChain让你不依赖大量手写代码就能搭出LLM应用。
LangChain的发展演变
这里有必要提一下版本演变,因为网上很多教程是基于旧版API写的,跑不动。
- LangChain v0.0.x(2022-2023):最早版本,API设计比较随意
- LangChain v0.1.x(2023):引入了LCEL(LangChain Expression Language),用管道操作符
|把组件串起来 - LangChain v0.2.x(2024至今):进一步统一了API,强化了Runnable接口
本书所有代码示例基于 v0.2.x+,如果你在跑代码时遇到API报错,先检查版本。
# 安装指定版本
pip install langchain>=0.2.0 langchain-openai langchain-community
核心概念:Runnable接口
从v0.1开始,LangChain所有组件都实现了 Runnable 接口,这是理解现代LangChain的关键。
Runnable 的核心方法:
# 每个Runnable都支持这四种调用方式
runnable.invoke(input) # 同步调用,返回最终结果
runnable.ainvoke(input) # 异步调用
runnable.batch([inp1, inp2]) # 批量调用
runnable.stream(input) # 流式输出
更重要的是,Runnable支持管道操作符 |,让你可以把多个组件像Unix管道一样串起来:
# 这就是LCEL(LangChain Expression Language)
chain = prompt | llm | output_parser
result = chain.invoke({"question": "什么是RAG?"})
这个设计非常优雅——prompt的输出自动变成llm的输入,llm的输出自动变成output_parser的输入,不需要手写中间变量。
为什么需要这个技术? 总结一句话:
LangChain把LLM应用开发从"手写每个细节"提升到了"组合标准组件"的层次,大幅降低了重复代码量,也提升了可维护性。
6.1.2 Model I/O:模型交互标准化
痛点再描述一下。
如果你直接调OpenAI的API,代码大概是这样:
import openai
response = openai.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "你好"}],
temperature=0.7,
max_tokens=1000
)
print(response.choices[0].message.content)
这看起来没问题,但一旦你要:
- 换模型(从GPT-4换到Claude,或者换到开源的Qwen)
- 批量处理100条数据
- 接入RAG(需要把检索结果拼进Prompt)
- 做流式输出
原来的代码就要大面积改动。
LangChain的Model I/O层就是来解决这个"模型接入碎片化"的问题的。
Model I/O三层架构
┌──────────────────────────────────────┐
│ Model I/O │
│ │
│ ┌────────────────────────────┐ │
│ │ Chat Models │ │
│ │ (对话模型,最常用) │ │
│ └────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────┐ │
│ │ LLM(Text Models) │ │
│ │ (纯文本补全模型) │ │
│ └────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────┐ │
│ │ Embeddings │ │
│ │ (文本向量化模型) │ │
│ └────────────────────────────┘ │
└──────────────────────────────────────┘
Chat Models:对话模型(最常用)
现代大模型应用几乎都是基于对话格式(Messages)的,LangChain用 ChatModel 统一了所有模型的调用接口。
代码实战6.1:统一调用多种大模型
"""
代码实战6.1:使用LangChain的ChatModel统一调用多种大模型
无论底层是OpenAI、Claude还是开源模型,代码写法完全一致
"""
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from langchain_community.chat_models import ChatZhipuAI # 智谱AI
from langchain_core.messages import HumanMessage, SystemMessage
from typing import List, Dict, Any
def demo_chat_model_unified():
"""
演示如何用统一的接口调用不同的大模型
核心思想:换了模型,业务代码不需要改
"""
# ========== 方式1:OpenAI GPT-4 ==========
# 只需要改这一行,后面的代码完全不用动
chat_model = ChatOpenAI(
model="gpt-4-turbo",
temperature=0.7,
max_tokens=1000,
# api_key="sk-..." # 也可以通过环境变量OPENAI_API_KEY设置
)
# 构造消息列表(标准格式,所有模型都认这个格式)
messages = [
SystemMessage(content="你是一个有帮助的AI助手,回答简洁明了。"),
HumanMessage(content="请用一句话解释什么是RAG")
]
# 调用模型(同步)
response = chat_model.invoke(messages)
print("GPT-4 回复:")
print(response.content)
print("-" * 50)
# ========== 方式2:切换到Anthropic Claude(只需要改模型初始化)==========
# 把上面那一行的ChatOpenAI换成ChatAnthropic
# chat_model = ChatAnthropic(
# model="claude-3-opus-20240229",
# temperature=0.7,
# max_tokens=1000
# )
# 后面的 messages = [...] 和 response = chat_model.invoke(messages) 完全不用改!
return response.content
def demo_streaming_output():
"""
演示流式输出:大模型边生成边返回,用户体验更好
所有ChatModel都支持stream方法
"""
chat_model = ChatOpenAI(model="gpt-3.5-turbo", streaming=True)
messages = [HumanMessage(content="从1数到10,每个数字一行")]
print("流式输出演示:")
for chunk in chat_model.stream(messages):
# chunk.content 是每个流式片段的内容
if chunk.content:
print(chunk.content, end="", flush=True)
print() # 换行
def demo_async_batch():
"""
演示批量异步调用:同时处理多个请求,大幅提升吞吐量
适合批量数据处理场景
"""
import asyncio
chat_model = ChatOpenAI(model="gpt-3.5-turbo")
messages_list = [
[HumanMessage(content="解释一下深度学习")],
[HumanMessage(content="解释一下强化学习")],
[HumanMessage(content="解释一下迁移学习")]
]
# 批量调用(自动并发)
responses = chat_model.batch(messages_list)
for i, response in enumerate(responses):
print(f"任务{i+1}结果(前50字):{response.content[:50]}...")
if __name__ == "__main__":
print("=" * 60)
print("代码实战6.1:统一调用多种大模型")
print("=" * 60)
demo_chat_model_unified()
# demo_streaming_output() # 取消注释可测试流式输出
关键点:invoke() 方法返回的是一个 AIMessage 对象,不只是字符串。它包含:
response.content # 模型生成的文字内容(最常用的字段)
response.additional_kwargs # 额外信息(如token使用量)
response.response_metadata # 响应元数据
LLM(Text Models):纯文本补全模型
这是更早期的接口,用于那些只支持"纯文本补全"的模型(如GPT-2、部分开源模型)。现在大多数场景直接用Chat Models就行。
from langchain_openai import OpenAI # 注意:这是文本补全接口,不是Chat!
llm = OpenAI(model="gpt-3.5-turbo-instruct")
result = llm.invoke("解释什么是深度学习")
print(result)
Embeddings:文本向量化模型
RAG应用中,把文本转成向量是核心步骤。LangChain用 Embeddings 接口统一了所有嵌入模型。
代码实战6.2:使用Embeddings接口统一调用嵌入模型
"""
代码实战6.2:使用LangChain的Embeddings接口
支持OpenAI、HuggingFace、本地模型等多种嵌入模型
"""
from langchain_openai import OpenAIEmbeddings
from langchain_community.embeddings import HuggingFaceEmbeddings
from typing import List
import numpy as np
def demo_embeddings_comparison():
"""
对比不同嵌入模型的使用方式
核心发现:接口完全一致,换模型只需要改初始化那一行
"""
# ========== 方式1:OpenAI嵌入模型 ==========
# 需要网络,需要API Key
openai_embeddings = OpenAIEmbeddings(
model="text-embedding-3-small", # 推荐用这个,性价比高
# dimensions=1024 # 可以指定输出维度(节省存储和计算)
)
texts = ["深度学习是机器学习的一个分支", "大语言模型改变了AI的格局"]
# embed_documents: 批量处理多个文本(推荐,省API调用次数)
embeddings = openai_embeddings.embed_documents(texts)
print(f"OpenAI嵌入维度:{len(embeddings[0])}")
print(f"生成了 {len(embeddings)} 个向量")
# embed_query: 专门处理查询文本(有些模型对查询和文档用不同的编码方式)
query_embedding = openai_embeddings.embed_query("什么是深度学习?")
print(f"查询向量维度:{len(query_embedding)}")
# ========== 方式2:HuggingFace开源嵌入模型(本地运行,无需API Key)==========
# 适合对数据隐私要求高的场景
# 首次运行会自动下载模型(约400MB)
hf_embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-large-zh-v1.5", # 中文效果很好的开源模型
model_kwargs={"device": "cpu"}, # 有GPU的话改成 "cuda"
encode_kwargs={"normalize_embeddings": True} # 归一化,使点积=余弦相似度
)
hf_embeddings_result = hf_embeddings.embed_documents(texts)
print(f"\nBGE嵌入维度:{len(hf_embeddings_result[0])}")
# 计算两个文本的相似度(余弦相似度)
vec1 = np.array(hf_embeddings_result[0])
vec2 = np.array(hf_embeddings_result[1])
cosine_sim = np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
print(f"两个文本的余弦相似度:{cosine_sim:.4f}")
return embeddings
if __name__ == "__main__":
print("=" * 60)
print("代码实战6.2:统一调用嵌入模型")
print("=" * 60)
demo_embeddings_comparison()
模型调用过程中的错误处理与重试
生产环境中,模型API调用可能因为网络抖动、限流、超时等原因失败。LangChain内置了重试机制:
from langchain_openai import ChatOpenAI
# 配置自动重试
chat_model = ChatOpenAI(
model="gpt-4-turbo",
max_retries=3, # 最多重试3次
request_timeout=30, # 单次请求超时30秒
# LangChain会自动处理:网络错误、速率限制错误等
)
更精细的控制可以用 tenacity 库自己包装,但大多数场景用上面内置的参数就够了。
6.1.3 Retrieval:构建RAG应用
Retrieval是LangChain中专门用于RAG(检索增强生成) 的模块。它把"检索"抽象成了标准组件,可以和ChatModel、Chain无缝集成。
RAG在LangChain中的完整数据流转
用户提问
↓
【RetrievalStep 1】加载文档(Document Loader)
↓
【RetrievalStep 2】文档分块(Text Splitter)
↓
【RetrievalStep 3】文本向量化(Embeddings)
↓
【RetrievalStep 4】存入向量数据库(VectorStore)
↓
【查询时】用户问题向量化 → 相似度搜索 → 取Top-K个相关文档
↓
【RetrievalStep 5】将相关文档拼进Prompt
↓
【RetrievalStep 6】发给LLM生成答案
↓
返回最终答案
Step 1 & 2:加载文档与分块
LangChain提供了大量的DocumentLoader,支持PDF、Word、Markdown、网页、Notion等格式。
代码实战6.3:从零构建RAG检索系统
"""
代码实战6.3:使用LangChain构建完整的RAG检索系统
包含:文档加载 → 分块 → 向量化 → 存储 → 检索
"""
from langchain_community.document_loaders import TextLoader, PDFMinerLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from typing import List, Dict
import os
def build_rag_retrieval_pipeline(
file_path: str,
persist_directory: str = "./chroma_db"
):
"""
构建RAG检索流水线的完整流程
Args:
file_path: 文档路径(支持.txt, .pdf等)
persist_directory: 向量数据库持久化目录
Returns:
retriever: 检索器对象,可以直接用来检索相关文档
"""
# ========== Step 1:加载文档 ==========
print(f"Step 1:加载文档 {file_path} ...")
if file_path.endswith(".txt"):
loader = TextLoader(file_path, encoding="utf-8")
elif file_path.endswith(".pdf"):
loader = PDFMinerLoader(file_path)
else:
raise ValueError(f"不支持的文件格式:{file_path}")
documents = loader.load()
print(f" 加载完成,共 {len(documents)} 个文档")
# ========== Step 2:文档分块 ==========
print("Step 2:文档分块...")
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 每个块约500字符
chunk_overlap=100, # 块之间重叠100字符(避免语义断裂)
length_function=len,
separators=["\n\n", "\n", "。", ";", ",", " ", ""] # 优先在句子边界切分
)
splits = text_splitter.split_documents(documents)
print(f" 分块完成,共 {len(splits)} 个块")
print(f" 块大小统计:平均约 {sum(len(s.page_content) for s in splits) // len(splits)} 字符")
# ========== Step 3:文本向量化 ==========
print("Step 3:初始化嵌入模型...")
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# ========== Step 4:存入向量数据库 ==========
print(f"Step 4:存入向量数据库(持久化到 {persist_directory})...")
# Chroma会自动处理:向量化 + 存储 + 建立索引
vectorstore = Chroma.from_documents(
documents=splits,
embedding=embeddings,
persist_directory=persist_directory
)
print(f" 存储完成,数据库中共有 {vectorstore._collection.count()} 个向量")
# ========== 返回检索器(Retriever)==========
# Retriever是LangChain中对"检索"的标准抽象
retriever = vectorstore.as_retriever(
search_type="similarity", # 相似度搜索
search_kwargs={"k": 3} # 返回最相关的3个块
)
print(" RAG检索流水线构建完成!")
return retriever
def demo_retrieval(retriever, query: str):
"""
演示如何使用检索器
"""
print(f"\n检索查询:{query}")
docs = retriever.invoke(query)
print(f"检索到 {len(docs)} 个相关文档块:")
for i, doc in enumerate(docs, 1):
print(f"\n 块{i}(前100字):{doc.page_content[:100]}...")
print(f" 元数据:{doc.metadata}")
if __name__ == "__main__":
print("=" * 60)
print("代码实战6.3:构建RAG检索系统")
print("=" * 60)
# 注意:需要准备一个测试文档,或者用手动构造的文档来测试
# 这里演示用字符串构造文档
from langchain_core.documents import Document
test_docs = [
Document(page_content="RAG(Retrieval-Augmented Generation)是一种结合检索和生成的技术。它先从外部知识库检索相关信息,然后基于检索到的内容生成答案。", metadata={"source": "RAG介绍"}),
Document(page_content="LangChain是一个用于开发LLM应用的框架。它提供了ChatModel、Chain、Agent、Retrieval等核心组件。", metadata={"source": "LangChain介绍"}),
Document(page_content="向量数据库用于存储文本的向量表示,支持高效的相似度搜索。常见的向量数据库有Chroma、Pinecone、Milvus等。", metadata={"source": "向量数据库介绍"}),
]
# 跳过文件加载,直接用测试文档演示检索
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
documents=test_docs,
embedding=embeddings,
persist_directory="./test_chroma_db"
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
demo_retrieval(retriever, "什么是RAG?")
Step 5 & 6:将检索结果接入LLM(RAG Chain)
有了Retriever,接下来要把"检索"和"生成"连起来,形成完整的RAG链路。LangChain用 create_retrieval_chain 或者LCEL来优雅地实现这个。
代码实战6.4:构建端到端RAG Chain
"""
代码实战6.4:使用LCEL构建端到端RAG Chain
将检索器和ChatModel用管道操作符连接起来
"""
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_core.documents import Document
def format_docs(docs):
"""
将检索到的文档块格式化为字符串
这是RAG Prompt中context的来源
"""
return "\n\n".join(doc.page_content for doc in docs)
def build_rag_chain():
"""
使用LCEL构建RAG Chain
数据流:问题 → 检索相关文档 → 填入Prompt → LLM生成答案
"""
# ========== 准备检索器(复用代码实战6.3的逻辑)==========
test_docs = [
Document(page_content="RAG(Retrieval-Augmented Generation)是一种结合检索和生成的技术。它先从外部知识库检索相关信息,然后基于检索到的内容生成答案。RAG可以有效减少大模型的幻觉问题。", metadata={"source": "RAG介绍"}),
Document(page_content="LangChain是一个用于开发LLM应用的框架。它提供了ChatModel、Chain、Agent、Retrieval等核心组件。LangChain支持多种大模型,包括OpenAI、Anthropic、HuggingFace等。", metadata={"source": "LangChain介绍"}),
Document(page_content="向量数据库用于存储文本的向量表示,支持高效的相似度搜索。常见的向量数据库有Chroma(轻量级,适合本地开发)、Pinecone(云服务)、Milvus(高性能分布式)。", metadata={"source": "向量数据库介绍"}),
Document(page_content="Agent是大模型应用的重要方向。Agent可以自主规划、调用工具、执行动作。主流的Agent范式包括ReAct、Plan-and-Execute、Tree-of-Thought等。", metadata={"source": "Agent介绍"}),
]
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
documents=test_docs,
embedding=embeddings,
persist_directory="./test_chroma_db"
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
# ========== 准备LLM ==========
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
# ========== 构建Prompt模板 ==========
# 这是RAG的核心Prompt,格式直接影响生成质量
prompt = ChatPromptTemplate.from_template("""
请基于以下参考文档回答用户的问题。
如果参考文档中没有相关信息,请明确说明"根据现有资料无法回答",不要编造答案。
【参考文档】
{context}
【用户问题】
{question}
【回答要求】
1. 答案要基于参考文档中的内容
2. 语言简洁明了
3. 如果信息不足,明确说明
""".strip())
# ========== 使用LCEL构建RAG Chain ==========
# 这是整段代码最核心的部分,理解数据是怎么流的:
#
# 输入:{"question": "用户问题"}
# ↓
# RunnablePassthrough.assign: 在输入字典中新增一个"context"键
# - context的值 = retriever.invoke(question) → format_docs → 格式化字符串
# ↓
# prompt: 把{"question": ..., "context": ...} 填入Prompt模板
# ↓
# llm: 把Prompt发给LLM
# ↓
# StrOutputParser: 把LLM的AIMessage转成纯字符串
#
# 用LCEL的管道操作符 | 把这些步骤串起来
rag_chain = (
{
"context": retriever | format_docs, # 检索相关文档并格式化
"question": RunnablePassthrough() # 原样传递用户问题
}
| prompt
| llm
| StrOutputParser()
)
return rag_chain, retriever
def demo_rag_chain(rag_chain):
"""演示RAG Chain的效果"""
questions = [
"什么是RAG?",
"LangChain支持哪些大模型?",
"有哪些常见的向量数据库?",
"Agent有哪些主流范式?" # 这个问题在参考文档里有答案
]
for question in questions:
print(f"\n问题:{question}")
answer = rag_chain.invoke({"question": question})
print(f"答案:{answer}")
if __name__ == "__main__":
print("=" * 60)
print("代码实战6.4:端到端RAG Chain")
print("=" * 60)
rag_chain, retriever = build_rag_chain()
demo_rag_chain(rag_chain)
这段代码是整个RAG部分最核心的实战代码。理解LCEL的数据流是理解现代LangChain的关键:
输入字典 {"question": "什么是RAG?"}
↓
retriever | format_docs
→ 执行检索,拿到相关文档,格式化成字符串
→ 结果存入字典的 "context" 键
↓
{"question": "...", "context": "RAG是..."}
↓
prompt.format(question=..., context=...)
→ 生成最终Prompt字符串
↓
llm.invoke(prompt)
→ 生成AIMessage
↓
StrOutputParser().parse(ai_message)
→ 提取出纯文本字符串
↓
返回最终答案
6.1.4 Chain:构建可组合的工作流
Chain是LangChain最早的核心概念,字面意思就是"把多个处理步骤链起来"。
在LCEL出现之前,Chain是用特定的类(如LLMChain、ConversationChain)来定义的。现在更推荐使用LCEL来构建Chain,因为它更灵活、更统一。
几种常见的Chain模式
模式1:最简单的Prompt → LLM Chain
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
# 用LCEL构建最简单的Chain
prompt = ChatPromptTemplate.from_template("{topic}领域的三个关键技术是什么?")
llm = ChatOpenAI(model="gpt-3.5-turbo")
output_parser = StrOutputParser()
chain = prompt | llm | output_parser
result = chain.invoke({"topic": "大模型"})
模式2:多步Chain(上一步的输出是下一步的输入)
from langchain_core.runnables import RunnablePassthrough
# 场景:先生成大纲,再基于大纲生成完整文章
outline_prompt = ChatPromptTemplate.from_template("请为"{topic}"这个主题生成一个文章大纲,包含3个章节")
article_prompt = ChatPromptTemplate.from_template("基于以下大纲,写一篇详细文章:\n\n{outline}")
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)
# Chain 1:生成大纲
outline_chain = outline_prompt | llm | StrOutputParser()
# Chain 2:基于大纲生成文章(把Chain 1的输出接进来)
# RunnablePassthrough() 把输入原样传递,这里用来把outline_chain的输出接给article_prompt
article_chain = (
outline_chain # 第一步:生成大纲
| (lambda outline: {"outline": outline}) # 把大纲包装成字典
| article_prompt # 填入文章生成Prompt
| llm
| StrOutputParser()
)
result = article_chain.invoke({"topic": "RAG技术"})
模式3:并行执行多个Chain,然后合并结果
from langchain_core.runnables import RunnableParallel
# 场景:同时让LLM从多个角度分析问题,然后汇总
parallel_chain = RunnableParallel({
" pros": pros_prompt | llm | StrOutputParser(), # 分支1:分析优点
"cons": cons_prompt | llm | StrOutputParser() # 分支2:分析缺点
})
# parallel_chain.invoke({"topic": "大模型"}) 会返回:
# {"pros": "优点...", "cons": "缺点..."}
# 再接一个汇总Prompt
summary_chain = (
parallel_chain
| summary_prompt
| llm
| StrOutputParser()
)
代码实战6.5:构建多步RAG增强的问答Chain
"""
代码实战6.5:构建带查询改写的多步RAG Chain
问题:用户的原始提问可能表述不清,先改写问题,再做RAG检索
"""
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough, RunnableLambda
from langchain_core.output_parsers import StrOutputParser
from langchain_core.documents import Document
def build_multi_step_rag_chain():
"""
构建多步RAG Chain:
Step 1: 查询改写(让LLM把用户问题改得更适合检索)
Step 2: 基于改写后的问题做RAG检索
Step 3: 基于检索结果生成答案
"""
# 准备测试数据
docs = [
Document(page_content="LangChain支持多种大模型接口,包括OpenAI、Anthropic Claude、HuggingFace模型等。通过统一的ChatModel接口,可以用相同的代码调用不同的模型。", metadata={"source": "模型接入"}),
Document(page_content="RAG(检索增强生成)通过从外部知识库检索相关信息,然后基于检索内容生成答案,有效减少大模型幻觉。RAG的核心步骤包括:文档加载、分块、向量化、检索、生成。", metadata={"source": "RAG原理"}),
Document(page_content="Agent可以自主规划、调用工具、执行动作。ReAct是一种主流Agent范式,让模型在每一步交替进行思考和行动。AutoGen是微软推出的多Agent协作框架。", metadata={"source": "Agent"})
]
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(docs, embeddings, persist_directory="./multi_step_rag_db")
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
# ========== Step 1:查询改写Chain ==========
rewrite_prompt = ChatPromptTemplate.from_template("""
原始用户问题:{question}
请把上面的问题改写得更加清晰、完整,使其更适合用于知识库检索。
要求:
1. 保留原问题的核心意图
2. 补充可能缺失的上下文
3. 直接输出改写后的问题,不要有任何解释
改写后的问题:""".strip())
rewrite_chain = rewrite_prompt | llm | StrOutputParser()
# ========== Step 2:RAG检索 + 生成 Chain ==========
rag_prompt = ChatPromptTemplate.from_template("""
请基于以下参考文档回答用户问题。
参考文档:
{context}
用户问题:{question}
回答:""".strip())
# 完整的多步Chain
# 技巧:用RunnableLambda包装retriever,使其能在LCEL中使用
def retrieve_and_format(question: str) -> str:
docs = retriever.invoke(question)
return "\n\n".join(doc.page_content for doc in docs)
full_chain = (
{
# "rewritten_question": 先改写问题(这一步可选,演示多步能力)
"context": RunnableLambda(retrieve_and_format),
"question": RunnablePassthrough()
}
| rag_prompt
| llm
| StrOutputParser()
)
return full_chain, rewrite_chain, retriever
def demo_multi_step_rag():
"""演示多步RAG Chain"""
full_chain, rewrite_chain, retriever = build_multi_step_rag_chain()
# 测试1:直接RAG
question = "LangChain怎么接入不同的模型?"
print(f"问题:{question}")
answer = full_chain.invoke({"question": question})
print(f"答案:{answer}\n")
# 测试2:先看查询改写效果
print("查询改写演示:")
rewritten = rewrite_chain.invoke({"question": "LangChain咋接各种模型?"}) # 口语化问题
print(f"原问题:LangChain咋接各种模型?")
print(f"改写后:{rewritten}")
print(f"\n用改写后的问题检索:")
answer2 = full_chain.invoke({"question": rewritten})
print(f"答案:{answer2}")
if __name__ == "__main__":
print("=" * 60)
print("代码实战6.5:多步RAG Chain")
print("=" * 60)
demo_multi_step_rag()
6.1.5 Agent:构建智能代理
LangChain对Agent的支持是比较早的功能,但API在v0.1之后有较大变化。现在推荐使用 create_tool_calling_agent 来构建Agent。
LangChain中Agent的核心组件
┌────────────────────────────────────────┐
│ LangChain Agent │
│ │
│ ┌────────────────────────────┐ │
│ │ Tools(工具列表) │ │
│ │ - search_tool │ │
│ │ - calculator_tool │ │
│ │ - custom_tool │ │
│ └────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────┐ │
│ │ Agent(决策引擎) │ │
│ │ (基于LLM + 工具描述) │ │
│ └────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────┐ │
│ │ AgentExecutor(执行器) │ │
│ │ (循环执行:思考→行动→观察)│ │
│ └────────────────────────────┘ │
└────────────────────────────────────────┘
代码实战6.6:使用LangChain构建支持工具调用的Agent
"""
代码实战6.6:使用LangChain构建支持工具调用的ReAct Agent
完整展示:工具定义 → Agent初始化 → 执行器运行 → 观察结果
"""
from langchain_openai import ChatOpenAI
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.tools import Tool, BaseTool
from langchain_core.prompts import ChatPromptTemplate
from typing import Dict, Any, Optional
import math
# ========== 方式1:用@tool装饰器定义工具(推荐)==========
from langchain_core.tools import tool
@tool
def calculator(expression: str) -> str:
"""
计算数学表达式的值。
支持加减乘除、括号、幂运算(**)。
Args:
expression: 数学表达式字符串,如 "2 + 3 * 4" 或 "(10 + 5) / 3"
Returns:
计算结果的字符串表示
"""
try:
# 安全问题:eval有风险,生产环境应该用ast.literal_eval或专门的表达式解析库
result = eval(expression, {"__builtins__": {}}, {"math": math})
return f"计算结果:{result}"
except Exception as e:
return f"计算错误:{str(e)}"
@tool
def get_word_length(word: str) -> str:
"""
计算一个单词的长度。
Args:
word: 输入的单词
Returns:
单词长度的字符串表示
"""
return f"{word} 的长度是 {len(word)} 个字符"
# ========== 方式2:继承BaseTool自定义工具(更灵活)==========
class WeatherQueryTool(BaseTool):
"""查询城市天气的工具(模拟)"""
name = "get_weather"
description = "查询指定城市的当前天气。当用户询问天气信息时使用此工具。输入应该是城市名称,如'北京'、'上海'。"
def _run(self, city: str) -> str:
"""工具的实际执行逻辑"""
# 模拟天气查询(实际应该调用天气API)
weather_data = {
"北京": "晴,25°C",
"上海": "多云,28°C",
"深圳": "雷阵雨,30°C"
}
return weather_data.get(city, f"未找到{city}的天气信息")
async def _arun(self, city: str) -> str:
"""异步版本(可选实现)"""
return self._run(city)
def build_agent_with_tools():
"""
构建支持工具调用的Agent
"""
# ========== 准备工具列表 ==========
tools = [calculator, get_word_length, WeatherQueryTool()]
# ========== 准备LLM ==========
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
# ========== 构建Agent的Prompt ==========
# 这个Prompt告诉Agent它有哪些工具可用,以及应该如何行动
prompt = ChatPromptTemplate.from_messages([
("system", """你是一个有用的AI助手,可以使用工具来回答用户问题。
当你需要查询信息或进行计算时,使用提供的工具。
如果工具返回了结果,基于结果回答用户问题。
如果没有相关工具或不需要工具就能回答,直接回答。"""),
("human", "{input}"),
("placeholder", "{agent_scratchpad}") # Agent的执行轨迹会填在这里
])
# ========== 创建Agent ==========
# create_tool_calling_agent 是LangChain推荐的新版API
agent = create_tool_calling_agent(llm, tools, prompt)
# ========== 创建执行器 ==========
# AgentExecutor负责循环执行:把Agent的决策变成实际行动
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True, # 打印执行过程(调试时有用)
max_iterations=5, # 最多执行5轮工具调用(防止无限循环)
handle_parsing_errors=True # 自动处理LLM输出格式错误
)
return agent_executor
def demo_agent_execution(agent_executor):
"""演示Agent的执行过程"""
test_queries = [
"帮我说一下北京今天天气怎么样?",
"计算一下 (10 + 5) * 3 等于多少?",
"hello 这个单词有多少个字符?"
]
for query in test_queries:
print("\n" + "=" * 60)
print(f"用户问题:{query}")
print("=" * 60)
result = agent_executor.invoke({"input": query})
print(f"\n最终答案:{result['output']}")
if __name__ == "__main__":
print("=" * 60)
print("代码实战6.6:LangChain工具调用Agent")
print("=" * 60)
agent_executor = build_agent_with_tools()
demo_agent_execution(agent_executor)
运行这段代码时,设置 verbose=True 会打印出Agent的完整思考过程:
> Entering new AgentExecutor chain...
Invoking: `get_weather` with `{'city': '北京'}`
晴,25°C
Invoking: `calculator` with `{'expression': '(10 + 5) * 3'}`
计算结果:45
Final Answer: 北京今天天气晴,25°C。(10 + 5) * 3 = 45
> Finished chain.
这就是ReAct范式在LangChain中的具体体现:思考(决定调用哪个工具)→ 行动(执行工具)→ 观察(拿到工具结果)→ 再思考……直到给出最终答案。
6.2 AutoGen多Agent框架实战
6.2.1 AutoGen设计哲学与核心概念
LangChain适合构建"单Agent应用",但当你需要多个Agent协作时,LangChain的原生支持就比较薄弱了。
微软研究院推出的AutoGen就是专门为多Agent协作设计的框架。它的核心思想是:
把每个Agent看成是一个"对话参与者",Agent之间通过发消息来协作,就像人在群里聊天一样。
AutoGen vs LangChain Agent:核心差异
| 维度 | LangChain Agent | AutoGen |
|---|---|---|
| 设计焦点 | 单个Agent的工具调用能力 | 多个Agent之间的对话协作 |
| Agent定义 | 基于Tools + Prompt | 基于ConversableAgent(可对话的Agent) |
| 协作模式 | 单一Agent循环执行 | 多个Agent发消息、形成对话网络 |
| 人类介入 | 支持,但需要自己写逻辑 | 原生支持人类反馈和介入 |
| 适用场景 | RAG、单步工具调用 | 代码生成、复杂任务分解、多角色协作 |
AutoGen的核心抽象
AutoGen里一切都是ConversableAgent——“可以对话的Agent”。每个Agent可以:
- 接收消息
- 思考(调用LLM)
- 发送消息给另一个Agent
- 执行工具(如果配置了
function_map) - 人类介入(如果配置了
human_input_mode)
┌────────────────────────────────────────────────┐
│ AutoGen 多Agent系统 │
│ │
│ Agent 1 ◄──消息──► Agent 2 │
│ │ │
│ │ 消息 │
│ ▼ │
│ Agent 3 ◄──消息──► Human(人类介入点) │
│ │
│ 每个Agent都是ConversableAgent的实例 │
│ 通过配置决定:是否调用LLM、是否有工具、是否求助人类│
└────────────────────────────────────────────────┘
6.2.2 ConversableAgent与群聊管理
ConversableAgent是AutoGen最核心的类。理解了这个类,就理解了AutoGen。
ConversableAgent的关键配置参数
from autogen import ConversableAgent
agent = ConversableAgent(
name="助手", # Agent的名字(在对话中显示)
llm_config={"config_list": [{"model": "gpt-4", "api_key": "..."}]}, # LLM配置
system_message="你是一个有帮助的助手", # 系统提示词
human_input_mode="NEVER", # 是否需要人类介入(NEVER/ALWAYS/TERMINATE)
function_map={"calculator": calc_func}, # 注册工具
code_execution_config=False, # 是否允许执行代码
max_consecutive_auto_reply=10, # 最多连续自动回复多少次(防止无限循环)
)
human_input_mode 有三个选项,这是AutoGen的一个很巧妙的设计:
"NEVER":完全自动,不需要人类介入(适合全自动流程)"ALWAYS":每一步都需要人类确认(适合高风险操作)"TERMINATE":只在对话要结束时请求人类确认(平衡点)
代码实战6.7:构建双Agent协作系统(代码生成 + 代码审查)
"""
代码实战6.7:使用AutoGen构建双Agent协作系统
场景:Coder Agent写代码 → Reviewer Agent审查代码 → 反馈修改
"""
import autogen
from autogen import ConversableAgent
from typing import Dict, List
def setup_autogen_agents(model_config: Dict):
"""
初始化两个协作的Agent:
- Coder:负责写代码
- Reviewer:负责审查代码,提出修改建议
Args:
model_config: LLM配置字典,格式:
{"config_list": [{"model": "gpt-4", "api_key": "sk-..."}]}
"""
# ========== Agent 1:Coder(代码编写者)==========
coder = ConversableAgent(
name="Coder",
llm_config=model_config,
system_message="""你是一个Python代码编写专家。
你的任务:根据用户需求编写高质量Python代码。
要求:
1. 代码要包含详细的中文注释
2. 遵循PEP8规范
3. 考虑边界条件和错误处理
4. 写完代码后,把代码发送给Reviewer审查""",
human_input_mode="NEVER", # 不需要人类介入
code_execution_config=False, # 不执行代码(只写,不跑)
)
# ========== Agent 2:Reviewer(代码审查者)==========
reviewer = ConversableAgent(
name="Reviewer",
llm_config=model_config,
system_message="""你是一个严格的Python代码审查专家。
你的任务:审查Coder提交的代码,检查:
1. 是否有Bug或边界条件处理不当
2. 注释是否足够清晰
3. 是否有可以优化的地方
4. 是否符合PEP8规范
如果代码质量合格,回复"APPROVED";
如果发现问题,详细说明问题并建议修改方案,然后把修改建议发回给Coder。""",
human_input_mode="NEVER",
)
return coder, reviewer
def run_coding_workflow(coder, reviewer, task_description: str):
"""
启动双Agent协作流程
数据流:
用户任务 → Coder写代码 → 发给Reviewer审查
↓ (如果APPROVED) 结束
↓ (如果没通过) Reviewer发修改建议 → Coder修改 → 再审查...
"""
print("\n" + "=" * 60)
print("启动双Agent协作:代码编写 + 代码审查")
print("=" * 60)
# 让Coder开始工作,并把结果发给Reviewer
# initiate_chat 是AutoGen中启动对话的方法
result = coder.initiate_chat(
recipient=reviewer,
message=f"请帮我写一个Python函数,实现快速排序(QuickSort),要求包含详细中文注释。",
max_turns=3 # 最多来回3轮(防止无限讨论)
)
return result
if __name__ == "__main__":
print("=" * 60)
print("代码实战6.7:AutoGen双Agent协作")
print("=" * 60)
# 注意:需要设置OPENAI_API_KEY环境变量,或者直接在config中填入
import os
model_config = {
"config_list": [{
"model": "gpt-3.5-turbo",
"api_key": os.getenv("OPENAI_API_KEY")
}]
}
if not model_config["config_list"][0]["api_key"]:
print("请先设置环境变量 OPENAI_API_KEY")
else:
coder, reviewer = setup_autogen_agents(model_config)
run_coding_workflow(coder, reviewer, "实现快速排序")
GroupChat:多Agent群聊管理
两个Agent对话用 initiate_chat 就够了,但如果有3个、4个甚至更多Agent呢?
AutoGen提供了 GroupChat 和 GroupChatManager 来管理多Agent群聊:
from autogen import GroupChat, GroupChatManager
# 创建多个Agent
coder = ConversableAgent(name="Coder", ...)
reviewer = ConversableAgent(name="Reviewer", ...)
tester = ConversableAgent(name="Tester", ...) # 新增:测试者
documenter = ConversableAgent(name="Documenter", ...) # 新增:文档编写者
# 创建群聊,指定参与者和发言顺序策略
groupchat = GroupChat(
agents=[coder, reviewer, tester, documenter],
messages=[],
max_round=10, # 最多10轮对话
# speaker_selection_method: 决定下一轮谁发言
# "auto":由LLM自动决定下一发言者(最灵活)
# "round_robin":按顺序轮流发言
speaker_selection_method="auto"
)
# 创建群聊管理器(也是一个Agent,负责协调整个群聊)
manager = GroupChatManager(
groupchat=groupchat,
llm_config=model_config
)
# 启动群聊:让Coder发起讨论
coder.initiate_chat(manager, message="我们需要实现一个用户登录功能,请大家协作完成")
GroupChat 的核心价值在于:自动决定下一轮谁发言。用 "speaker_selection_method": "auto" 时,Manager会让LLM根据当前对话内容来判断"接下来谁该说话",这比固定顺序要灵活得多。
6.2.3 人类反馈与干预机制
这是AutoGen相比其他框架的一大优势:人类对Agent的控制是原生支持的,不是事后打补丁。
三种人类介入模式详解
模式1:"NEVER" — 全自动模式
agent = ConversableAgent(
name="全自动Agent",
llm_config=model_config,
human_input_mode="NEVER" # 完全不需要人类确认
)
# 适用场景:批量处理、低风险任务、已充分测试的流程
模式2:"ALWAYS" — 每步确认模式
agent = ConversableAgent(
name="谨慎Agent",
llm_config=model_config,
human_input_mode="ALWAYS" # 每轮都请求人类输入
)
# 适用场景:高风险操作(删除文件、发送邮件、金融交易)
# 运行时会暂停,等待人类在终端输入:
# - 输入内容 → 作为人类反馈发给Agent
# - 输入 "exit" → 终止对话
模式3:"TERMINATE" — 智能介入模式(推荐)
agent = ConversableAgent(
name="智能介入Agent",
llm_config=model_config,
human_input_mode="TERMINATE" # 只在Agent认为需要人类确认时才介入
)
# Agent在决定"任务完成了"的时候,会请求人类最终确认
# 适用场景:大多数生产环境
用人类反馈来纠正Agent的错误
# 场景:Agent生成了代码,但人类发现有个Bug,可以在ALWAYS模式下纠正
# 运行时会提示:
# > Human Input (回车跳过,输入内容发送,'exit'终止):
# 你可以输入:
# "第三个函数里有个off-by-one错误,请修正"
# Agent会收到这个反馈,然后修改代码
6.2.4 AutoGen实战:代码生成与调试Agent
这部分做一个相对完整的实战项目:让多个AutoGen Agent协作完成"需求 → 代码 → 测试 → 调试"的完整流程。
代码实战6.8:完整的代码生成与调试多Agent系统
"""
代码实战6.8:AutoGen多Agent代码生成与调试完整流程
涉及4个Agent角色:
1. ProductManager:把用户需求拆解成技术规格
2. Coder:根据技术规格写代码
3. Tester:写测试用例,发现Bug
4. Debugger:分析测试失败原因,指导Coder修复
"""
import autogen
from autogen import ConversableAgent, GroupChat, GroupChatManager
from typing import Dict
def create_coding_team(model_config: Dict):
"""
创建一个完整的代码生成与调试团队
每个Agent有明确的角色定位和退出条件
"""
# ========== Agent 1:产品经理(需求分析)==========
pm = ConversableAgent(
name="ProductManager",
llm_config=model_config,
system_message="""你是一个产品经理AI。
你的输入:用户的模糊需求(如"帮我做个待办事项应用")
你的输出:清晰的技术规格说明,包括:
1. 核心功能列表
2. 每个功能的输入输出
3. 边界条件和异常处理要求
4. 建议的代码结构
输出格式要清晰,方便Coder直接基于此写代码。""",
human_input_mode="NEVER",
)
# ========== Agent 2:程序员(代码编写)==========
coder = ConversableAgent(
name="Coder",
llm_config=model_config,
system_message="""你是一个Python代码编写专家。
你的任务:根据ProductManager提供的技术规格,编写完整的Python代码。
要求:
1. 包含详细的中文注释
2. 遵循PEP8规范
3. 包含所有必要的异常处理
4. 代码要可以直接运行
你写完代码后,把代码发给Tester进行测试。""",
human_input_mode="NEVER",
code_execution_config={
"work_dir": "autogen_workspace", # 代码执行的工作目录
"use_docker": False # 不用Docker(生产环境建议用Docker沙箱)
} # 允许执行代码!
)
# ========== Agent 3:测试工程师(写测试、运行测试)==========
tester = ConversableAgent(
name="Tester",
llm_config=model_config,
system_message="""你是一个严格的测试工程师AI。
你的任务:
1. 根据技术规格,为Coder的代码编写完整的测试用例
2. 运行测试用例(你可以执行代码)
3. 如果测试全部通过,回复"ALL_TESTS_PASSED"
4. 如果有测试失败,详细描述了失败原因,并把失败信息发给Debugger
你生成的测试代码要包含:
- 正常情况的测试
- 边界情况的测试
- 异常情况的测试""",
human_input_mode="NEVER",
code_execution_config={
"work_dir": "autogen_workspace",
"use_docker": False
}
)
# ========== Agent 4:调试工程师(分析错误、指导修复)==========
debugger = ConversableAgent(
name="Debugger",
llm_config=model_config,
system_message="""你是一个调试专家AI。
你的任务:
1. 分析Tester报告的测试失败信息
2. 定位代码中的Bug原因
3. 给出具体的修复建议(要详细到Coder可以直接改)
4. 把修复建议发给Coder
如果经过3轮调试仍然无法修复,回复"DEBUG_FAILED: [原因]",终止流程。""",
human_input_mode="NEVER",
)
return pm, coder, tester, debugger
def run_full_coding_workflow(model_config: Dict, user_requirement: str):
"""
运行完整的代码生成与调试流程
数据流:
用户需求 → PM分析 → Coder写代码 → Tester测试
↓ (测试失败) → Debugger分析 → Coder修改 → Tester再测试
↓ (测试通过) → 输出最终代码
"""
pm, coder, tester, debugger = create_coding_team(model_config)
# 创建GroupChat,让4个Agent协作
groupchat = GroupChat(
agents=[pm, coder, tester, debugger],
messages=[],
max_round=15, # 最多15轮对话
speaker_selection_method="auto" # LLM自动选择下一发言者
)
manager = GroupChatManager(
groupchat=groupchat,
llm_config=model_config
)
print("\n" + "=" * 60)
print("启动完整的代码生成与调试多Agent系统")
print(f"用户需求:{user_requirement}")
print("=" * 60)
# PM发起讨论
result = pm.initiate_chat(
manager,
message=f"新需求来了:{user_requirement}\n请先分析需求,输出技术规格。",
max_turns=15
)
return result
if __name__ == "__main__":
print("=" * 60)
print("代码实战6.8:AutoGen完整代码生成与调试系统")
print("=" * 60)
import os
model_config = {
"config_list": [{
"model": "gpt-3.5-turbo",
"api_key": os.getenv("OPENAI_API_KEY")
}]
}
if not model_config["config_list"][0]["api_key"]:
print("请先设置环境变量 OPENAI_API_KEY")
else:
# 运行完整流程
result = run_full_coding_workflow(
model_config,
user_requirement="写一个Python函数,实现二分查找算法,并在有序数组中查找目标元素"
)
print("\n最终对话历史:")
for msg in result.chat_history:
print(f"[{msg['role']}] {msg['content'][:100]}...")
6.3 框架选型与对比
6.3.1 LangChain vs AutoGen:适用场景对比
这两个框架经常被拿来比较,但它们的定位其实不太一样。
| 维度 | LangChain | AutoGen |
|---|---|---|
| 核心定位 | LLM应用开发的全能框架 | 多Agent协作的专业框架 |
| 最佳场景 | RAG应用、单Agent工具调用、Prompt工程 | 多角色协作任务、代码生成、复杂问题分解 |
| 学习曲线 | 中等(概念多,API演变快) | 较平缓(核心概念少) |
| 工具调用 | 非常完善(Tool、Toolkit、AgentExecutor) | 支持,但不是核心焦点 |
| 多Agent协作 | 基础支持(需要自己编排) | 核心能力(GroupChat原生支持) |
| 人类介入 | 需要自己实现 | 原生支持(human_input_mode) |
| 代码执行 | 需要自己集成 | 原生支持(code_execution_config) |
| 社区&生态 | 非常庞大(最多人用) | 中等(微软背书,增长快) |
| 生产稳定性 | 较好(很多公司在用) | 较好(微软内部在用) |
选型建议:
- 做RAG应用 → 选LangChain(Retrieval模块非常成熟)
- 做单Agent工具调用 → LangChain和AutoGen都可以,LangChain的工具定义更规范
- 做多Agent协作(如代码生成团队) → 选AutoGen(GroupChat是杀手级功能)
- 两个都用 → 实际上可以混合使用:用LangChain做RAG检索,把结果送给AutoGen的Agent处理
6.3.2 其他框架概览
除了LangChain和AutoGen,还有几个值得关注的框架:
CrewAI:角色驱动的多Agent框架
CrewAI的设计灵感来自"一个团队(Crew)里有不同角色的人,各自有专长,协作完成任务"。
# CrewAI的核心抽象
from crewai import Agent, Task, Crew
# 定义Agent(指定角色、目标、工具)
researcher = Agent(
role="高级研究员",
goal="收集关于某个技术的最新信息",
backstory="你是一个经验丰富的研究员,擅长信息检索和分析",
tools=[search_tool]
)
writer = Agent(
role="技术作家",
goal="把研究结果写成易懂的文章",
backstory="你是一个优秀的技术作家,擅长把复杂概念讲清楚"
)
# 定义Task(任务)
research_task = Task(
description="研究RAG技术的最新进展",
agent=researcher
)
write_task = Task(
description="基于研究结果写一篇科普文章",
agent=writer
)
# 组建Crew(团队),指定执行策略
crew = Crew(
agents=[researcher, writer],
tasks=[research_task, write_task],
process="sequential" # 顺序执行:先研究,再写文章
)
result = crew.kickoff()
CrewAI vs AutoGen:
- CrewAI更适合"角色职责明确、按顺序执行"的场景
- AutoGen更适合"需要灵活对话、动态决定下一发言人"的场景
Semantic Kernel:微软的另一个Agent框架
Semantic Kernel(SK)是微软推出的另一款框架,定位更偏向企业.NET/Java/Python全栈,和AutoGen是"同门师兄弟"。
# Semantic Kernel的核心抽象:Kernel + Plugin + Planner
import semantic_kernel as sk
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
kernel = sk.Kernel()
kernel.add_service(OpenAIChatCompletion(model_id="gpt-4", api_key="..."))
# Plugin = 工具集合(类似LangChain的Toolkit)
# Planner = 自动规划器(类似Agent的规划能力)
Semantic Kernel的特点:
- 对.NET/C#支持最好(如果用C#写后端,SK是首选)
- Plugin系统很规范(比LangChain的Tool定义更结构化)
- Planner功能比LangChain的Agent弱一些
Haystack:专注于RAG和搜索的框架
Haystack是deepset.ai推出的框架,专注在搜索+RAG场景,在NLP学术界用得比较多。
如果你要做的东西很接近"问答系统"、“文档检索”,Haystack可能是比LangChain更专业的选择。
6.3.3 自定义框架设计考虑
有时候,现成框架不能满足你的需求。比如:
- 需要极致的推理速度(框架的抽象层带来了额外延迟)
- 需要接入非常定制化的工具或数据源
- 需要在资源受限的环境运行(框架本身太重)
什么情况下应该自研框架?
# 一个极简的"自研Agent框架"核心循环,不到50行代码
def minimal_agent_loop(
llm_call, # 函数:调用LLM
tools: dict, # 字典:工具名 → 工具函数
system_prompt: str,
max_iterations: int = 10
):
"""
极简Agent循环:思考 → 行动 → 观察
适合对性能和可控性要求极高的场景
"""
messages = [{"role": "system", "content": system_prompt}]
for i in range(max_iterations):
# === 思考:让LLM决定下一步 ===
response = llm_call(messages)
if response.finish_reason == "stop":
# LLM认为任务完成了,直接返回最终答案
return response.content
# === 行动:解析工具调用 ===
tool_call = parse_tool_call(response) # 解析LLM输出中的工具调用
tool_name = tool_call["name"]
tool_params = tool_call["parameters"]
# === 执行:调用实际工具 ===
if tool_name in tools:
observation = tools[tool_name](**tool_params)
else:
observation = f"错误:工具 {tool_name} 不存在"
# === 观察:把结果加回消息历史 ===
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": f"工具执行结果:{observation}"})
return "达到最大迭代次数,任务可能未完成。"
自研框架的优劣势:
| 优势 | 劣势 |
|---|---|
| 极致性能(无框架开销) | 开发成本高,需要自己处理各种边界情况 |
| 完全可控(每个细节都能改) | 维护成本高,框架团队的工作你得自己做 |
| 可以深度优化特定场景 | 生态为零,没有社区插件可用 |
| 新员工需要学习你的自研框架(学习成本) |
建议:
- 90%的场景用现成框架(LangChain/AutoGen)
- 只有当你对性能、可控性有极端要求时,才考虑自研
- 如果自研,参考LangChain的Runnable接口设计和AutoGen的ConversableAgent设计,不要从头造轮子
6.3.4 框架性能与可扩展性评估
选框架时,性能是一个重要考量。但这里说的"性能"不止是"跑得快",还包括:
┌─────────────────────────────────────────────┐
│ 框架性能评估维度 │
│ │
│ 1. 推理延迟:从发请求到拿到完整回复的时间 │
│ 2. 吞吐量:单位时间内能处理多少请求 │
│ 3. 内存占用:框架本身占多少内存 │
│ 4. 可扩展性:能否方便地接入新工具/新模型 │
│ 5. 并发能力:能否同时处理多个用户请求 │
│ 6. 部署便利性:是否容易容器化、微服务化 │
└─────────────────────────────────────────────┘
推理延迟对比(定性分析)
直接调用API(无框架): 基准线(100%)
LangChain LCEL: +5~15%(主要是Prompt组装和输出解析的开销)
LangChain AgentExecutor: +20~40%(需要多轮循环,每轮都有Prompt组装开销)
AutoGen ConversableAgent: +15~30%(主要是消息历史管理开销)
自研极简循环: +5~10%
结论:框架带来的额外延迟通常不超过50ms,相比LLM本身的推理时间(几百ms到几秒),这个开销可以忽略。不要为了"性能"而牺牲框架的开发效率。
可扩展性:接入新模型/新工具有多容易?
# LangChain:接入新工具(非常简单)
@tool
def my_custom_tool(param: str) -> str:
"""工具描述,LLM会看到这个。"""
return do_something(param)
# LangChain:接入新模型(也比较简单)
from langchain_core.language_models import BaseChatModel
class MyCustomChatModel(BaseChatModel):
"""只需要实现_generate方法,就能接入任何自定义模型"""
def _generate(self, messages, stop=None, run_manager=None, **kwargs):
# 调用你的自定义模型API
response = my_custom_api(messages)
return AIMessage(content=response)
企业级避坑指南
避坑1:LangChain版本升级导致代码大面积报错
问题描述:
LangChain在2023-2024年间经历了两次较大的API变更(v0.0 → v0.1 LCEL,v0.1 → v0.2),网上的很多教程代码是基于旧版API的,直接抄来用会报错。
典型报错:
TypeError: LLMChain.__init__() got an unexpected keyword argument 'llm'
# 这是旧版API的写法,新版已经不支持
解决方案:
- 固定版本:在
requirements.txt中固定LangChain版本
langchain==0.2.0
langchain-openai==0.1.0
langchain-community==0.2.0
-
使用现代API:所有新代码都用LCEL(
prompt | llm | output_parser),不要用旧的LLMChain、ConversationChain等类 -
迁移旧代码:参考官方迁移指南
# 旧版(不要用)
from langchain.chains import LLMChain
chain = LLMChain(llm=llm, prompt=prompt)
result = chain.run(input="...")
# 新版(推荐)
chain = prompt | llm | StrOutputParser()
result = chain.invoke({"input": "..."})
避坑2:AutoGen的GroupChat陷入无限对话
问题描述:
配置了GroupChat后,多个Agent来回对话,但没有一个Agent"喊停",导致对话无限继续,API费用快速消耗。
解决方案:
groupchat = GroupChat(
agents=[agent1, agent2, agent3],
messages=[],
max_round=10, # ← 一定要设置这个!限制最多多少轮对话
speaker_selection_method="auto"
)
# 另外,在每个Agent的system_message里明确"退出条件":
system_message="""...
如果任务已完成,请明确说"TASK_COMPLETED",这样GroupChat就会结束。
不要在没有新信息的情况下重复之前的发言。"""
避坑3:框架抽象泄漏,调试困难
问题描述:
用框架写代码很爽,但一旦出问题(比如Agent不调用工具、RAG检索结果不对),很难定位是"框架的问题"还是"自己代码的问题",因为框架封装了很多细节。
解决方案:
- 开启详细日志:
import logging
# LangChain日志
logging.basicConfig(level=logging.DEBUG)
# AutoGen日志
autogen.logging_config = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {"default": {"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"}},
"handlers": {"default": {"class": "logging.StreamHandler", "formatter": "default"}},
"loggers": {"autogen": {"handlers": ["default"], "level": "DEBUG"}}
}
-
先不用框架写一遍原型,确认逻辑正确后,再用框架重构。这样你清楚"框架在帮你做什么",出问题时能快速定位。
-
为关键步骤写单元测试:
# 测试RAG检索是否正常工作(不依赖LLM)
def test_retriever():
retriever = build_retriever()
docs = retriever.invoke("测试查询")
assert len(docs) > 0, "检索器没有返回任何文档"
本章小结
核心Takeaways
-
LangChain的核心价值在于把LLM应用开发的通用模式抽象成标准组件(Model I/O、Retrieval、Chain、Agent、Memory),用LCEL可以把这些组件像管道一样串起来。
-
LCEL(LangChain Expression Language) 是现代LangChain的核心,用
prompt | llm | output_parser的管道语法替代了旧的LLMChain类,代码更清晰、更灵活。 -
RAG在LangChain中的完整流程:Document Loader → Text Splitter → Embeddings → VectorStore → Retriever → 填入Prompt → LLM生成答案,每一步都有对应的标准组件。
-
AutoGen的核心设计哲学是"Agent之间的对话即协作",
ConversableAgent可以发消息、调用工具、执行代码、请求人类介入,非常适合多Agent协作场景。 -
AutoGen的GroupChat 支持多个Agent自动协作,通过
speaker_selection_method="auto"让LLM动态决定下一轮谁发言,比固定顺序的协作模式灵活得多。 -
框架选型建议:RAG应用选LangChain,多Agent协作选AutoGen,两个可以混合使用。90%的场景不需要自研框架。
思考题
思考题1:
你在用LangChain构建一个RAG应用时,发现检索到的文档块和问题"语义相关但实际没用"(比如问题问"如何使用API",检索到了"API的架构设计",内容相关但不是用户想要的)。请设计一种检索后处理方案来提升最终答案质量,可以用到本章介绍的哪些LangChain组件?
参考答案要点核心思路:检索只是第一步,需要在检索之后、送给LLM之前,增加一个"重排序(Reranking)"步骤。
具体方案:
- 用LangChain的
Retriever先拉取Top-20(多拿一些) - 用一个轻量的Reranker模型(如Cohere Rerank、或BGE Reranker)对这20个文档块重新打分排序
- 取Reranker排序后的Top-5送给LLM
用到的LangChain组件:
- 可以自定义一个
BaseRetriever的子类,把Reranker逻辑封装进去 - 或者用LCEL:
retriever | reranker | format_docs | prompt | llm - LangChain社区版也提供了
ContextualCompressionRetriever,就是做这件事的
延伸:还可以结合"查询改写"(用LLM把用户问题改写成更适合检索的版本),进一步提升检索精度。
思考题2:
AutoGen的GroupChat中,如果有一个Agent"太能说"(比如Coder Agent每次都输出很长的代码解释,导致对话历史很快超出LLM的上下文窗口),请设计两种不同的策略来解决这个问题,并分析各自的优缺点。
参考答案要点策略1:限制每个Agent的单次输出长度
- 在Agent的
system_message里明确指示:“每次回复控制在200字以内,代码用代码块展示” - 优点:简单,不需要改框架代码
- 缺点:LLM不一定遵守,需要加输出解析器强制截断
策略2:用max_consecutive_auto_reply限制连续发言次数
coder = ConversableAgent(
name="Coder",
llm_config=model_config,
max_consecutive_auto_reply=2 # ← 最多连续说2次话
)
- 优点:框架原生支持,效果好
- 缺点:可能打断Agent的合理长回复(比如确实需要从头写到尾的代码)
策略3(推荐):启用code_execution_config,让代码在沙箱里执行,对话中只传"执行结果摘要"
- 优点:代码不占对话历史,只传执行结果
- 缺点:需要配置沙箱环境(Docker),部署复杂度增加
策略4:定期Summary(用另一个Agent负责总结对话历史,压缩上下文)
- AutoGen社区有这个功能的讨论,但尚未成熟,可以自己实现
更多推荐


所有评论(0)