AI 应用框架与编排学习博客(通俗原理 + 详细注释 · AI应用强化版)
LangChain 和 LlamaIndex 是构建 AI 应用的两大主流框架。这篇博客从实际问题出发 ,用生活化类比 建立直觉,通过术语详解 深入概念本质,再用原理剖析 和可运行代码带你一步步理解。每个知识点都聚焦在"什么时候用、为什么这样设计、如何调优"。
一、LangChain / LlamaIndex 快速上手
1. 框架定位与选择
问题
市面上有 LangChain、LlamaIndex 等多个框架,它们各自主攻什么方向?我该学哪个?
生活化类比
- LangChain 就像瑞士军刀------有各种各样的组件(链、工具、记忆、代理),能灵活组合搭建复杂工作流,但也可能被繁多的选择弄得眼花。
- LlamaIndex 就像专业的图书馆系统------专注于把文档整理成索引,提供高效的检索接口,在 RAG 场景下开箱即用。
术语详解
- LangChain :通用 LLM 应用框架,核心抽象是
Chain(将多个步骤串联)和Agent(让 LLM 自主决策调用工具)。生态庞大,适合构建需要复杂编排的应用。 - LlamaIndex :专注数据索引和检索的框架,核心抽象是
QueryEngine和Retriever。在文档问答、知识库检索场景中,比 LangChain 更简洁。它也提供了ReActAgent等轻量 Agent 能力,但 Agent 生态和灵活性不如 LangChain 丰富,复杂编排场景仍推荐 LangChain。 - 实际选型:两者不是互斥的,经常混用------用 LlamaIndex 构建索引和检索,用 LangChain 做流程编排和工具调用。
| 维度 | LangChain | LlamaIndex |
|---|---|---|
| 核心场景 | 通用编排、Agent、工具调用 | 文档索引、检索、RAG |
| Agent 能力 | 丰富(ReAct、OpenAI、Plan-and-Execute 等) | 轻量(ReActAgent) |
| 学习曲线 | 较陡(概念多) | 较平缓(聚焦检索) |
| 文档问答 | 需要手动组装 | 开箱即用 |
| 复杂工作流 | 原生支持 | 较弱 |
| 常用组合 | 编排 + 工具 | 索引 + 检索 |
2. 初级:Chain 构建与 Prompt 模板
问题
一个 LLM 应用通常需要多步处理------先格式化提示词,再调用模型,最后解析输出。如何优雅地串联这些步骤?
生活化类比
Chain 就像工厂流水线:每个工位做一件事(格式化 → 推理 → 提取),物料从一个工位流向下一个,最终输出成品。
术语详解
- Chain :将多个组件按顺序串联的执行单元。最简单的
LLMChain包含一个 Prompt 模板和一个 LLM。 - Prompt 模板 :用
{变量}占位的提示词,调用时动态填入具体内容。 - Parser:从 LLM 的文本输出中提取结构化数据。
原理
Chain 的核心思想是"声明式组装"------你定义好每一步做什么,框架负责按序执行。LLMChain 内部先调用 Prompt 模板的 format() 填充变量,再调用 LLM 生成,最后用 Parser 解析。更复杂的 Chain 还可以串联多个 LLM 调用或插入检索步骤。
演示用例:用 LangChain 构建一个翻译 Chain
python
# pip install langchain langchain-openai
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema.output_parser import StrOutputParser
# 1. 定义 LLM
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.3)
# 2. 定义 Prompt 模板
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个专业的{source_lang}到{target_lang}翻译助手。只返回翻译结果,不要解释。"),
("user", "{text}")
])
# 3. 组装 Chain(Prompt → LLM → Parser)
chain = prompt | llm | StrOutputParser()
# 4. 调用 Chain(单个输入)
result = chain.invoke({
"source_lang": "中文",
"target_lang": "英文",
"text": "检索增强生成是让大模型回答新知识的核心架构"
})
print("翻译结果:", result)
# 5. 批量处理多个输入(chain.batch)
inputs = [
{"source_lang": "中文", "target_lang": "英文", "text": "你好世界"},
{"source_lang": "中文", "target_lang": "英文", "text": "机器学习很有趣"},
]
batch_results = chain.batch(inputs)
print("批量结果:", batch_results)
输出结果
翻译结果: Retrieval-Augmented Generation is the core architecture for enabling large models to answer questions based on new knowledge.
批量结果: ['Hello World', 'Machine learning is very interesting']
演示用例:用 LlamaIndex 快速搭建文档问答
python
# pip install llama-index llama-index-embeddings-openai
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
# 1. 加载文档目录下的所有文件
documents = SimpleDirectoryReader("./docs").load_data()
print(f"加载文档数:{len(documents)}")
# 2. 构建索引(自动分块、Embedding、存储)
index = VectorStoreIndex.from_documents(documents)
print("索引构建完成")
# 3. 创建查询引擎
query_engine = index.as_query_engine(similarity_top_k=3)
# 4. 提问
response = query_engine.query("什么是 RAG?")
print("回答:", response)
输出结果
加载文档数:5
索引构建完成
回答: RAG(检索增强生成)是一种结合信息检索与文本生成的技术架构...
AI 应用场景 :Chain 适合需要多步处理的场景(翻译、摘要、提取),LlamaIndex 适合文档问答。两者可混用:用 LlamaIndex 检索,用 LangChain 编排后续处理。
batch()方法用于批量处理,能显著提升吞吐量。
3. 中级:LCEL 表达式、自定义工具、回调与追踪
问题
简单 Chain 只能串行执行,如何实现更复杂的流程------比如并行调用多个模型、条件分支、流式输出?
生活化类比
LCEL 就像乐高积木的接口规范------所有积木(组件)都遵循统一的插拔标准,你可以把检索器、模型、解析器像积木一样任意拼接。
术语详解
- LCEL(LangChain Expression Language) :用
|管道符串联组件,声明式定义执行流程。 - RunnableParallel:并行执行多个分支。
- RunnableBranch:条件分支,根据输入动态选择执行路径。
- 回调(Callback):在 Chain 执行过程中插入钩子函数,用于日志记录、成本追踪、调试。多个回调按注册顺序依次执行。
- 追踪(Tracing):记录每次调用的完整链路(输入、输出、耗时、token 用量)。常用 LangSmith 等工具。追踪的核心价值是定位性能瓶颈------比如发现"检索耗时 2 秒但 LLM 调用只有 0.5 秒",就能针对性优化检索策略。
原理深入:Runnable 协议
LCEL 的核心是 Runnable 协议------所有组件(Prompt 模板、LLM、Parser、Retriever)都实现了统一的接口:
| 方法 | 用途 | 适用场景 |
|---|---|---|
invoke(input) |
同步执行,返回完整结果 | 脚本、简单请求 |
ainvoke(input) |
异步执行 | Web 服务中不阻塞事件循环 |
stream(input) |
同步流式,逐块产出 | 本地调试流式输出 |
astream(input) |
异步流式 | Web 服务中的流式接口 |
batch(inputs) |
批量并行处理多个输入 | 离线批处理 |
管道符 | 实际调用 Runnable.__or__() 方法,把前一个组件的输出自动传给下一个组件。LangChain 内部会检查类型兼容性------如果一个组件输出的类型与下一个组件期望的输入类型不匹配,会在运行时抛出清晰的错误。
如果你想自定义一个 Runnable 组件 ,只需继承 Runnable 并实现 invoke() 方法(以及可选的 astream 等),就能无缝接入 LCEL 管道。
演示用例:LCEL 并行调用 + 条件分支
python
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema.output_parser import StrOutputParser
from langchain.schema.runnable import RunnableParallel, RunnableBranch
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
# 并行分支:同时调用两个不同风格的翻译
british_prompt = ChatPromptTemplate.from_template("Translate to British English: {text}")
american_prompt = ChatPromptTemplate.from_template("Translate to American English: {text}")
parallel_chain = RunnableParallel(
british=british_prompt | llm | StrOutputParser(),
american=american_prompt | llm | StrOutputParser()
)
text = "今天天气真好,适合出去玩"
results = parallel_chain.invoke({"text": text})
print("英式翻译:", results["british"])
print("美式翻译:", results["american"])
# 条件分支:根据文本长度选择模型
long_text_prompt = ChatPromptTemplate.from_template("Summarize: {text}")
short_text_prompt = ChatPromptTemplate.from_template("Translate to English: {text}")
branch_chain = RunnableBranch(
(lambda x: len(x["text"]) > 100, long_text_prompt | llm | StrOutputParser()),
short_text_prompt | llm | StrOutputParser() # 默认分支
)
print("条件分支结果:", branch_chain.invoke({"text": "你好世界"}))
输出结果
英式翻译: The weather is lovely today, perfect for going out.
美式翻译: The weather is really nice today, perfect for going out.
条件分支结果: Hello World
演示用例:回调记录 token 用量
python
from langchain.callbacks import BaseCallbackHandler
class CostTracker(BaseCallbackHandler):
"""自定义回调:记录每次 LLM 调用的 token 消耗"""
total_tokens = 0
def on_llm_end(self, response, **kwargs):
tokens = response.llm_output.get("token_usage", {}).get("total_tokens", 0)
self.total_tokens += tokens
print(f"本次消耗 {tokens} tokens,累计 {self.total_tokens} tokens")
tracker = CostTracker()
llm_with_callback = ChatOpenAI(model="gpt-3.5-turbo", callbacks=[tracker])
# 使用带回调的 LLM
result = llm_with_callback.invoke("写一句鼓励的话")
print("回复:", result.content)
输出结果
本次消耗 42 tokens,累计 42 tokens
回复: 每一步的前进都是成长,坚持努力,你会遇见更好的自己。
AI 应用场景:LCEL 并行调用适合对比多个模型输出、批量处理;回调用于成本监控和调试;追踪是生产环境的必备能力,便于定位链路上的性能瓶颈(如发现检索步骤耗时异常,可针对性优化)。
4. 中级:流式处理
问题
大模型生成内容可能需要数秒甚至更久,如果等全部生成完再返回,用户会感觉卡顿。如何实现"边生成边输出"的流式体验?
生活化类比
流式处理就像打字机------敲一个字母出一个字母,而不是等整篇文章写完再给你看。用户能实时看到生成进度,体验流畅。
术语详解
- stream(同步流) :
chain.stream(input)返回同步迭代器,适合本地脚本。 - astream(异步流) :
chain.astream(input)返回异步迭代器,适合 Web 服务中不阻塞事件循环。 - SSE 流式接口 :在 Web 服务中,用
StreamingResponse将流式生成的事件推送给前端。
原理
LLM 的流式模式在底层返回一个迭代器,每生成一个 token 就 yield 出来。LangChain 的 stream() 封装了这个迭代器。在 Web 服务中,配合 StreamingResponse 和 SSE 协议,把每个 chunk 包装成 data: {chunk}\n\n 推送给前端。生产环境需处理生成中途出错的情况------通过 try/except 捕获异常,向客户端发送 data: [ERROR]\n\n 事件,避免连接挂起。
演示用例:LangChain 流式输出
python
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema.output_parser import StrOutputParser
llm = ChatOpenAI(model="gpt-3.5-turbo", streaming=True) # 开启流式
prompt = ChatPromptTemplate.from_template("用100字介绍{topic}")
chain = prompt | llm | StrOutputParser()
print("流式输出:")
for chunk in chain.stream({"topic": "检索增强生成"}):
print(chunk, end="", flush=True) # flush=True 立即显示
print()
输出结果(逐字出现)
流式输出:
检索增强生成(RAG)是一种结合信息检索与文本生成的技术架构...
在 FastAPI 中提供流式接口(含错误处理)
python
from fastapi import FastAPI
from starlette.responses import StreamingResponse
app = FastAPI()
async def generate_stream(topic: str):
"""异步生成器,逐块产出文本,中途出错时发送错误事件"""
try:
async for chunk in chain.astream({"topic": topic}):
yield f"data: {chunk}\n\n"
yield "data: [DONE]\n\n"
except Exception as e:
# 发生错误时通知客户端,而非直接断开连接
yield f"data: [ERROR] {str(e)}\n\n"
@app.get("/stream")
async def stream_endpoint(topic: str):
return StreamingResponse(
generate_stream(topic),
media_type="text/event-stream"
)
AI 应用场景 :流式处理是 ChatGPT 式对话体验的核心。
stream()用于本地脚本调试,astream()用于 Web 服务。生产环境务必处理流式生成中途的异常,向客户端发送明确的错误事件。
二、工具集成与函数调用
1. 初级:让 LLM 调用外部 API、计算器、搜索
问题
LLM 被训练数据截止日期限制,无法获取实时信息(天气、股票、搜索)。如何给它"接上网线"?
生活化类比
工具调用就像给人配手机:人本身知识有限,但有了手机就能随时搜索、计算、查天气。LLM 也一样,配上工具就能突破自身局限。
术语详解
- Function Calling / Tool Calling:LLM 根据用户问题,自动判断是否需要调用外部工具,并生成符合工具要求的参数。你执行函数后把结果返回,模型再生成最终回答。
- 工具定义:描述工具的名称、功能、参数 Schema(JSON Schema 格式)。
- 工具执行流程:用户提问 → LLM 判断需不需要调用工具 → 如果需要,返回工具名和参数 → 你执行工具 → 把结果发给 LLM → LLM 生成最终回复。
原理
LLM 本身不能联网,但 Function Calling 机制让它能"表示"调用意图。你在代码中检测 LLM 返回的 tool_calls,执行真正的函数(如调天气 API),然后把结果作为新消息发给 LLM,完成闭环。注意:LLM 不直接调用工具,它只生成参数,实际执行由你的代码完成。
演示用例:让 LLM 使用计算器
python
from openai import OpenAI
import json
import math
client = OpenAI()
# 定义工具
tools = [{
"type": "function",
"function": {
"name": "calculator",
"description": "执行数学计算,支持加减乘除和平方根",
"parameters": {
"type": "object",
"properties": {
"expression": {"type": "string", "description": "数学表达式,如 'sqrt(16)+3*2'"},
"operation": {"type": "string", "enum": ["add","subtract","multiply","divide","sqrt"]}
},
"required": ["expression"]
}
}
}]
def execute_calculator(expression: str) -> float:
"""执行数学表达式(⚠️ 仅演示,生产环境必须用沙箱)"""
allowed_names = {"sqrt": math.sqrt, "abs": abs, "round": round}
return eval(expression, {"__builtins__": {}}, allowed_names)
# 用户提问
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "16的平方根加上3乘以2等于多少?"}],
tools=tools,
tool_choice="auto"
)
msg = response.choices[0].message
if msg.tool_calls:
tool_call = msg.tool_calls[0]
args = json.loads(tool_call.function.arguments)
print("LLM 想调用:", tool_call.function.name, "参数:", args)
# 执行工具
result = execute_calculator(args["expression"])
print("计算结果:", result)
# 把结果返回给 LLM
final_response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "user", "content": "16的平方根加上3乘以2等于多少?"},
msg, # LLM 的工具调用消息
{"role": "tool", "tool_call_id": tool_call.id, "content": str(result)}
]
)
print("最终回答:", final_response.choices[0].message.content)
输出结果
LLM 想调用: calculator 参数: {'expression': 'sqrt(16)+3*2'}
计算结果: 10.0
最终回答: 16的平方根是4,3乘以2等于6,相加得到10。
AI 应用场景:工具调用让 LLM 能查询实时数据、操作数据库、调用第三方 API。这是构建 AI Agent 的基础------模型不再只是"说",还能"做"。
2. 中级:工具描述设计、错误重试、并行调用、安全沙箱
问题
工具多了以后,模型可能选错工具、参数传错、调用失败,如何设计健壮的工具系统?
2.1 工具描述设计
核心原则:工具描述是模型"看懂"工具能力的唯一途径,需要清晰、具体、包含边界。
设计清单:
| 要素 | 说明 | 坏例子 | 好例子 |
|---|---|---|---|
| 名称 | 动词+名词,见名知意 | do_stuff |
search_weather |
| 描述 | 一句话说明做什么 + 适用场景 | "搜索" | "搜索指定城市的实时天气,返回温度和湿度" |
| 参数名 | 语义明确 | q |
city_name |
| 参数描述 | 说明格式和约束 | "字符串" | "城市中文名称,如'北京'或'上海'" |
| 枚举值 | 限定可选范围 | operation: string |
`operation: "add" |
2.2 错误重试
问题:工具调用可能因为网络波动、API 限流、参数格式错误而失败。
关键区分:
| 错误类型 | 示例 | 是否重试 | 原因 |
|---|---|---|---|
| 瞬时错误 | 网络超时、API 限流(429)、服务暂时不可用(503) | ✅ 重试 | 短时间内可能恢复 |
| 永久错误 | 参数格式错、权限不足(403)、资源不存在(404) | ❌ 不重试 | 重试多少次都一样 |
python
import time
import requests
def call_tool_with_retry(tool_func, args, max_retries=3, backoff=2):
"""带指数退避的重试机制(仅重试瞬时错误)"""
for attempt in range(max_retries):
try:
return tool_func(**args)
except requests.Timeout as e:
# 网络超时 → 可重试
if attempt == max_retries - 1:
return f"工具调用超时(已重试{max_retries}次)"
wait_time = backoff ** attempt
print(f"超时,{wait_time}秒后重试...")
time.sleep(wait_time)
except (ValueError, KeyError, TypeError) as e:
# 参数错误 → 不可重试,直接返回错误
return f"参数错误(不重试):{str(e)}"
except Exception as e:
# 未知错误 → 谨慎重试一次
if attempt == max_retries - 1:
return f"工具调用失败:{str(e)}"
time.sleep(backoff ** attempt)
2.3 并行调用
问题:用户问"北京和上海的天气分别怎么样",模型需要调用两次天气工具,串行调用耗时长。
解决方案 :检测 LLM 返回的 tool_calls 是否包含多个工具调用,如果多个且相互独立(参数不依赖彼此结果),就并行执行。
python
import asyncio
async def execute_tools_parallel(tool_calls, tool_map):
"""并行执行多个独立的工具调用"""
async def execute_one(call):
func = tool_map[call.function.name]
args = json.loads(call.function.arguments)
try:
return await func(**args)
except Exception as e:
# return_exceptions=True 让失败不影响其他工具
return {"error": str(e), "tool": call.function.name}
tasks = [execute_one(call) for call in tool_calls]
results = await asyncio.gather(*tasks, return_exceptions=True)
return results
适用场景 :多个独立的查询(同时查天气、股价)、多个独立的计算。return_exceptions=True 确保某个工具失败不会中断其他工具的调用------你可以将成功和失败的结果汇总后一起发给 LLM,让它根据部分成功的结果生成回答或重新决策。
2.4 安全沙箱
⚠️ 生产环境警告:本章节的
eval示例仅用于本地调试理解原理。生产环境严禁使用eval执行任何用户相关或 LLM 生成的代码,必须使用 Docker 沙箱或专用沙箱服务(如 E2B、OpenAI Code Interpreter)。
问题:如果让 LLM 生成代码并执行(如计算器场景),恶意输入可能导致任意代码执行。
防御层级:
| 层级 | 措施 | 作用 |
|---|---|---|
| 1 | 白名单限制导入 | 只允许 math、datetime 等安全模块 |
| 2 | 禁用 __builtins__ |
阻止 open()、__import__() 等危险函数 |
| 3 | 超时机制 | 防止死循环耗尽资源 |
| 4 | 资源限制 | 限制内存、CPU 使用 |
| 5 | Docker 沙箱 | 生产级方案:在隔离容器中执行 |
三、面试模拟题
1. 对比型:LangChain 和 LlamaIndex 分别适合什么场景?你会怎么选?
答案要点:LangChain 适合需要复杂编排的场景(多步 Chain、Agent、工具调用),LlamaIndex 专注文档索引和检索。LlamaIndex 也有轻量 Agent 能力,但复杂编排仍推荐 LangChain。实际常混用------LlamaIndex 做检索,LangChain 做编排。选择取决于你的核心需求是"编排"还是"检索"。
2. 原理型 :LCEL 管道符 | 的背后是什么机制?如果我想自定义一个 Runnable 组件,需要实现哪些方法?
答案要点 :LCEL 基于 Runnable 协议,所有组件实现统一的 invoke()/stream()/batch()/ainvoke() 等接口。| 调用 __or__() 方法,把前一个组件的输出自动传给下一个组件。自定义组件只需继承 Runnable 并实现 invoke() 方法,就能无缝接入管道。
3. 场景型:你的工具调用有时失败(网络超时),有时模型选错工具。你从哪些方面优化?
答案要点:工具描述清晰化(名称、参数说明、枚举值);重试机制中区分瞬时错误(网络超时可重试)和永久错误(参数格式错不重试);工具数量过多时考虑分组或让模型只调用最相关的工具;并行执行独立的多个调用以减少总耗时;某个工具失败不影响其他并行调用。
4. 原理型:Function Calling 中,LLM 真的"调用"了外部 API 吗?完整的闭环是怎样的?
答案要点:LLM 不直接调用工具。它只根据用户问题判断是否需要调用,并生成符合工具定义的参数。实际执行由你的代码完成,结果返回给 LLM 后,LLM 再生成最终回答。整个过程是:用户提问 → LLM 判断 → 返回工具调用请求 → 你执行 → 返回结果 → LLM 生成回复。
5. 场景型:你的 AI 应用需要让用户看到"逐字输出"的流式体验,但如果 LLM 中途出错怎么办?
答案要点 :LLM 开启 streaming=True,后端用 chain.astream() 异步逐块获取 token,通过 FastAPI 的 StreamingResponse + SSE 推送给前端。关键是在生成器中使用 try/except 包裹,捕获异常后向客户端发送 data: [ERROR] {原因}\n\n 事件,避免连接挂起或静默失败。正常结束发送 data: [DONE]\n\n。
AI 应用场景速查表
| 知识点 | 核心用途 | 典型场景 |
|---|---|---|
| LangChain Chain | 多步串联 | 翻译→摘要→提取流水线 |
| LlamaIndex 索引 | 文档检索 | 知识库问答 |
| Runnable 协议 | 统一组件接口 | 自定义 LCEL 组件 |
| LCEL 并行 | 对比多个模型 | A/B 测试不同提示 |
| 回调追踪 | 成本监控、性能定位 | 生产环境调试优化 |
| 流式处理 | 实时输出体验 | ChatGPT 式对话 |
| Function Calling | 让 LLM 操作外部世界 | 搜索、计算、API 调用 |
| 工具描述设计 | 提高调用准确率 | 多工具 Agent |
| 重试机制 | 处理瞬时错误 | 网络不稳定的生产环境 |
| 并行调用 | 减少多工具总耗时 | 同时查天气和股价 |
| 安全沙箱 | 防代码注入 | 用户可输入计算式 |