LLM成长笔记(七): AI 应用框架与编排

AI 应用框架与编排学习博客(通俗原理 + 详细注释 · AI应用强化版)

LangChain 和 LlamaIndex 是构建 AI 应用的两大主流框架。这篇博客从实际问题出发 ,用生活化类比 建立直觉,通过术语详解 深入概念本质,再用原理剖析可运行代码带你一步步理解。每个知识点都聚焦在"什么时候用、为什么这样设计、如何调优"。


一、LangChain / LlamaIndex 快速上手

1. 框架定位与选择

问题

市面上有 LangChain、LlamaIndex 等多个框架,它们各自主攻什么方向?我该学哪个?

生活化类比

  • LangChain 就像瑞士军刀------有各种各样的组件(链、工具、记忆、代理),能灵活组合搭建复杂工作流,但也可能被繁多的选择弄得眼花。
  • LlamaIndex 就像专业的图书馆系统------专注于把文档整理成索引,提供高效的检索接口,在 RAG 场景下开箱即用。

术语详解

  • LangChain :通用 LLM 应用框架,核心抽象是 Chain(将多个步骤串联)和 Agent(让 LLM 自主决策调用工具)。生态庞大,适合构建需要复杂编排的应用。
  • LlamaIndex :专注数据索引和检索的框架,核心抽象是 QueryEngineRetriever。在文档问答、知识库检索场景中,比 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 白名单限制导入 只允许 mathdatetime 等安全模块
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
重试机制 处理瞬时错误 网络不稳定的生产环境
并行调用 减少多工具总耗时 同时查天气和股价
安全沙箱 防代码注入 用户可输入计算式
相关推荐
imbackneverdie4 小时前
好用的AI论文写作工具
人工智能·aigc·论文·科研·ai写作·ai工具
SCKJAI4 小时前
边缘AI新标杆,Pandora Orin NX16+GPT-OSS重新定义终端智能
人工智能·gpt
掘根4 小时前
【openCV】图像显示,色彩空间转换
人工智能·opencv·计算机视觉
海兰4 小时前
从原始日志到系统知识:补齐 AI 可观测性的“上下文层“
人工智能·elasticsearch
爱喝水的木子4 小时前
LearnPilot AI
人工智能
完成大叔4 小时前
从脚本到Agent:工具模式下的智能价值
人工智能·langchain
weixin_436182424 小时前
工业 AI 芯片如何选型?告别纸质手册,实现快速比对
人工智能·ai芯片·ai助手
searchforAI4 小时前
视频画面里的PPT怎么提取?视频转图文讲义的实操教程
人工智能·学习·ai·aigc·powerpoint·音视频·贴图
Rain5094 小时前
mini-cc:一个轻量级 AI 编程助手的诞生
人工智能·typescript·ai编程