第 7 章:Agent 框架实战

本章深入 Agent 框架的核心概念与实战应用。如果说上一章是学会"调用 API",本章就是学会"编排智能"——让 LLM 像前端组件一样被组合、复用和调度,构建真正能解决复杂问题的 AI 系统。

7.1 Agent 概念深化

Agent(智能体)是 AI 领域最热门的概念之一。但到底什么是 Agent?它和直接调用 LLM API 有什么区别?

Agent vs 普通 LLM 调用

普通 LLM 调用是"一问一答":你提问题,模型给答案。而 Agent 是"自主执行":你给目标,Agent 自己规划步骤、调用工具、观察结果、调整策略,直到完成任务。

维度普通 LLM 调用Agent
交互模式单轮/多轮对话规划-执行-观察循环
工具使用需手动处理自动选择并调用
错误处理人工介入自动重试、调整
记忆固定上下文长期记忆、向量检索
适用场景问答、生成复杂任务、自动化流程
ℹ️前端类比

普通 LLM 调用像静态页面——用户请求什么就返回什么;Agent 像完整的 Web 应用——有状态管理(Memory)、路由系统(Planning)、API 调用(Tools)和错误边界(Error Handling)。

规划-执行-观察循环

Agent 的核心工作模式是一个循环:

  1. 规划(Planning)

    Agent 分析目标,制定执行计划。例如"查天气"任务可能被分解为:确定城市 → 调用天气 API → 格式化结果。

  2. 执行(Action)

    按计划调用工具或执行代码。Agent 可能调用搜索引擎、数据库、计算器等外部能力。

  3. 观察(Observation)

    收集工具返回的结果,评估是否达到目标。如果未完成,进入下一轮规划。

ReAct 模式

ReAct(Reasoning + Acting)是当前最流行的 Agent 设计模式。它要求模型在每一步同时输出"思考过程"和"执行动作",形成交错推理。

text
用户:2024 年诺贝尔物理学奖得主是谁?他出生在哪一年?

思考 1:我需要查找 2024 年诺贝尔物理学奖得主的信息。
动作 1:search("2024 Nobel Prize Physics winner")
观察 1:2024 年诺贝尔物理学奖授予 John J. Hopfield 和 Geoffrey E. Hinton。

思考 2:我需要分别查找两位科学家的出生年份。
动作 2:search("John J. Hopfield born")
观察 2:John J. Hopfield 出生于 1933 年 7 月 15 日。

思考 3:现在查找 Geoffrey Hinton 的出生年份。
动作 3:search("Geoffrey Hinton born")
观察 3:Geoffrey Everest Hinton 出生于 1947 年 12 月 6 日。

思考 4:我已经获得了所有需要的信息,可以回答用户了。
动作 4:finish("2024 年诺贝尔物理学奖得主是 John J. Hopfield(1933 年出生)和 Geoffrey E. Hinton(1947 年出生)。")

7.2 LangChain 核心

LangChain 是 Python 生态中最流行的 LLM 应用框架。它的设计理念与前端工程化非常相似:组件化、可组合、可扩展。

ℹ️前端类比

LangChain 就像 LLM 界的 React:提供组件(Components)、状态管理(Memory)、路由(Chains/Routers)和工具集成(Tools),让你像搭积木一样构建 AI 应用。

安装 LangChain

bash
pip install langchain langchain-openai langchain-community

Chain 链式调用

Chain 是 LangChain 的核心抽象,表示"一系列有序操作"。就像前端的数据流管道,数据从一端流入,经过多个处理步骤,从另一端流出。

python
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

# 初始化模型(兼容 OpenAI 格式的 API 都可以)
llm = ChatOpenAI(
    model="gpt-4o-mini",
    api_key="sk-your-key",
    base_url="https://api.openai.com/v1"
)

# 构建 Prompt 模板
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个{role},用{style}风格回答问题。"),
    ("human", "{question}")
])

# 输出解析器:将模型输出转为字符串
output_parser = StrOutputParser()

# 组装 Chain:prompt -> llm -> parser
chain = prompt | llm | output_parser

# 调用 Chain(像调用函数一样)
result = chain.invoke({
    "role": "前端专家",
    "style": "幽默",
    "question": "为什么 CSS 居中这么难?"
})
print(result)

# 批量处理
questions = [
    {"role": "前端专家", "style": "简洁", "question": "什么是闭包?"},
    {"role": "后端专家", "style": "详细", "question": "什么是事务?"}
]
results = chain.batch(questions)
for r in results:
    print(r)

PromptTemplate

LangChain 提供了多种 Prompt 模板,让提示词管理更加工程化:

python
from langchain_core.prompts import (
    PromptTemplate,
    ChatPromptTemplate,
    MessagesPlaceholder,
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate
)

# 1. 简单字符串模板
prompt = PromptTemplate.from_template("""
将以下文本翻译成 {language}:
{text}
""")

# 2. 聊天消息模板(推荐)
chat_prompt = ChatPromptTemplate.from_messages([
    SystemMessagePromptTemplate.from_template(
        "你是一位资深 {domain} 工程师。"
    ),
    MessagesPlaceholder(variable_name="history"),  # 插入历史消息
    HumanMessagePromptTemplate.from_template("{input}")
])

# 3. 部分填充模板(复用)
code_reviewer_prompt = PromptTemplate.from_template("""
请审查以下 {language} 代码,关注:
1. 潜在 Bug
2. 性能问题
3. 可读性

代码:
```{language}
{code}
```
""")

# 创建一个专门审查 Python 的模板
python_reviewer = code_reviewer_prompt.partial(language="python")

Memory 记忆

LangChain 提供了多种 Memory 实现,让 Agent 能记住对话历史:

python
from langchain.memory import ConversationBufferMemory, ConversationSummaryMemory
from langchain.chains import ConversationChain

# 1. 缓冲区记忆:保存完整对话历史
buffer_memory = ConversationBufferMemory()

# 2. 摘要记忆:自动压缩历史(适合长对话)
summary_memory = ConversationSummaryMemory(
    llm=llm,
    max_token_limit=1000
)

# 创建带记忆的对话链
conversation = ConversationChain(
    llm=llm,
    memory=buffer_memory,
    verbose=True  # 打印执行过程
)

# 第一轮
response1 = conversation.predict(input="我叫小明,我喜欢 Python。")
print(response1)

# 第二轮:模型能记住"小明"这个名字
response2 = conversation.predict(input="我刚才说了我叫什么?")
print(response2)

# 查看记忆内容
print(buffer_memory.load_memory_variables({}))

Agents 与 Tools

LangChain 的 Agent 可以自动选择并调用工具:

python
from langchain.agents import create_react_agent, AgentExecutor
from langchain_core.tools import tool
from langchain import hub

# 1. 定义工具(用装饰器很方便)
@tool
def search_web(query: str) -> str:
    """搜索网络获取实时信息。当用户询问最新事件、新闻或你不知道的信息时使用。"""
    # 实际项目中接入搜索引擎 API
    return f"【模拟搜索结果】关于 '{query}' 的最新信息:..."

@tool
def calculate(expression: str) -> str:
    """执行数学计算。当用户需要进行数值计算时使用。"""
    try:
        return str(eval(expression))
    except Exception as e:
        return f"计算错误: {e}"

@tool
def get_current_time() -> str:
    """获取当前时间。"""
    from datetime import datetime
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

# 2. 工具列表
tools = [search_web, calculate, get_current_time]

# 3. 加载 ReAct Prompt 模板(带本地 fallback,避免网络问题)
try:
    prompt = hub.pull("hwchase17/react")
except Exception:
    # 网络不可用时使用本地模板
    from langchain_core.prompts import PromptTemplate
    prompt = PromptTemplate.from_template("""Answer the following questions as best you can. You have access to the following tools:

{tools}

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: {input}
Thought:{agent_scratchpad}""")

# 4. 创建 Agent
agent = create_react_agent(llm, tools, prompt)

# 5. 执行器
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,  # 打印思考过程
    handle_parsing_errors=True  # 自动处理解析错误
)

# 6. 运行
result = agent_executor.invoke({
    "input": "现在几点了?123 乘以 456 等于多少?"
})
print(result["output"])

7.3 LangChain 实战

构建一个多功能 Agent,能查天气(模拟)、算数学、写代码。

python
#!/usr/bin/env python3
"""
multi_tool_agent.py
多功能 Agent:天气查询 + 数学计算 + 代码生成
"""

import os
import json
from datetime import datetime
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain.agents import create_structured_chat_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from dotenv import load_dotenv

load_dotenv()

# 初始化 LLM
llm = ChatOpenAI(
    model=os.getenv("LLM_MODEL", "gpt-4o-mini"),
    api_key=os.getenv("OPENAI_API_KEY"),
    base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"),
    temperature=0.3
)

# ========== 工具定义 ==========

@tool
def get_weather(city: str) -> str:
    """获取指定城市的天气信息。当用户询问天气时使用。"""
    weather_data = {
        "北京": {"temp": 22, "condition": "晴", "humidity": 45},
        "上海": {"temp": 26, "condition": "多云", "humidity": 65},
        "深圳": {"temp": 30, "condition": "小雨", "humidity": 80},
        "杭州": {"temp": 24, "condition": "阴", "humidity": 55}
    }
    data = weather_data.get(city)
    if not data:
        return f"暂无 {city} 的天气数据,试试:北京、上海、深圳、杭州"
    return f"{city}今天{data['condition']},气温 {data['temp']}°C,湿度 {data['humidity']}%"

@tool
def calculate(expression: str) -> str:
    """执行数学计算。当用户需要进行加减乘除或复杂计算时使用。"""
    allowed = set("0123456789+-*/.() ")
    if not all(c in allowed for c in expression):
        return "表达式包含非法字符,只允许数字和运算符"
    try:
        result = eval(expression)
        return f"{expression} = {result}"
    except Exception as e:
        return f"计算错误: {e}"

@tool
def generate_code(description: str, language: str = "python") -> str:
    """生成代码示例。当用户要求写代码或需要代码示例时使用。"""
    # 实际场景中这里可以调用专门的代码生成模型
    return f"【代码生成器】将为您生成 {language} 代码:{description}"

tools = [get_weather, calculate, generate_code]

# ========== Agent 配置 ==========

system_template = """你是一个 helpful 的 AI 助手,可以使用工具帮助用户解决问题。

可用的工具:
{tools}

工具调用格式:
```json
{{"action": "工具名", "action_input": {{"参数名": "参数值"}}}}
```

当不需要工具时,直接回复用户。"""

prompt = ChatPromptTemplate.from_messages([
    ("system", system_template),
    MessagesPlaceholder(variable_name="chat_history", optional=True),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad")
])

# 创建 Agent
agent = create_structured_chat_agent(llm, tools, prompt)
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,
    handle_parsing_errors=True
)

# ========== 交互循环 ==========

def main():
    print("=" * 50)
    print("  多功能 AI Agent")
    print("  支持:天气查询 / 数学计算 / 代码生成")
    print("  输入 /quit 退出")
    print("=" * 50)
    print()

    while True:
        user_input = input("你: ").strip()
        if user_input == "/quit":
            break
        if not user_input:
            continue

        try:
            result = agent_executor.invoke({"input": user_input})
            print(f"\n助手: {result['output']}\n")
        except Exception as e:
            print(f"\n出错了: {e}\n")

if __name__ == "__main__":
    main()

7.4 AutoGen 多智能体

AutoGen 是微软开源的多智能体框架,核心思想是"对话式编程"——让多个 AI Agent 像团队成员一样协作完成任务。

ℹ️前端类比

AutoGen 就像一个敏捷开发团队:UserProxyAgent 是产品经理(提需求),AssistantAgent 是程序员(写代码),他们之间通过"对话"(消息传递)协作完成项目。

核心概念

bash
pip install pyautogen
python
import os
from autogen import ConversableAgent, UserProxyAgent

# 配置 LLM
config_list = [{
    "model": "gpt-4o-mini",
    "api_key": os.getenv("OPENAI_API_KEY"),
    "base_url": os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
}]

# 创建助手 Agent
assistant = ConversableAgent(
    name="代码助手",
    system_message="你是一位 Python 专家。编写简洁、可运行的代码,并解释关键部分。",
    llm_config={"config_list": config_list},
    human_input_mode="NEVER"
)

# 创建用户代理 Agent
user_proxy = UserProxyAgent(
    name="用户",
    human_input_mode="NEVER",
    max_consecutive_auto_reply=10,
    code_execution_config={
        "work_dir": "coding",
        "use_docker": False  # 本地执行,不用 Docker
    }
)

# 启动对话:用户代理向助手发送任务
user_proxy.initiate_chat(
    assistant,
    message="写一个 Python 脚本,计算斐波那契数列前 20 项,并保存到 fibonacci.txt"
)

# 输出:
# 助手会生成代码,用户代理会自动执行并返回结果
# 如果代码有错误,助手会修复,形成自动迭代

多 Agent 协作

python
from autogen import GroupChat, GroupChatManager

# 定义多个专家 Agent
coder = ConversableAgent(
    name="程序员",
    system_message="你负责编写代码。只输出代码,不输出解释。",
    llm_config={"config_list": config_list}
)

reviewer = ConversableAgent(
    name="代码审查员",
    system_message="你负责审查代码质量。检查 Bug、性能和可读性,提出改进建议。",
    llm_config={"config_list": config_list}
)

tester = ConversableAgent(
    name="测试员",
    system_message="你负责编写测试用例。确保代码覆盖边界情况。",
    llm_config={"config_list": config_list}
)

# 创建群聊
group_chat = GroupChat(
    agents=[user_proxy, coder, reviewer, tester],
    messages=[],
    max_round=12  # 最多 12 轮对话
)

manager = GroupChatManager(
    groupchat=group_chat,
    llm_config={"config_list": config_list}
)

# 启动协作
user_proxy.initiate_chat(
    manager,
    message="开发一个函数,验证邮箱地址格式,需要包含单元测试。"
)

7.5 MCP (Model Context Protocol)

MCP 是 Anthropic 于 2024 年底推出的开放协议,旨在标准化 LLM 与外部工具、数据源之间的通信方式。

为什么 MCP 重要

在 MCP 之前,每个框架(LangChain、AutoGen、LlamaIndex)都有自己的工具定义格式。开发者需要为不同框架重复编写工具适配代码。MCP 就像"AI 领域的 USB-C"——统一接口,一次编写,到处使用。

ℹ️前端类比

MCP 就像 REST API 标准化了前后端通信。以前每个后端框架有自己的 RPC 协议,REST 出现后,前端可以用统一的方式(HTTP + JSON)调用任何后端服务。

MCP Server 与 Client 架构

python
# MCP Server 示例(使用官方 Python SDK)
# pip install mcp

from mcp.server import Server
from mcp.types import TextContent
import mcp.server.stdio

server = Server("file-reader")

@server.list_tools()
async def list_tools():
    return [{
        "name": "read_file",
        "description": "读取文件内容",
        "inputSchema": {
            "type": "object",
            "properties": {
                "path": {"type": "string", "description": "文件路径"}
            },
            "required": ["path"]
        }
    }]

@server.call_tool()
async def call_tool(name, arguments):
    if name == "read_file":
        with open(arguments["path"], "r") as f:
            content = f.read()
        return [TextContent(type="text", text=content)]

# 启动 Server
if __name__ == "__main__":
    mcp.server.stdio.run_server(server)
💡发展趋势

MCP 生态正在快速发展,已有文件系统、数据库、浏览器、GitHub 等官方 Server。建议关注官方仓库 github.com/modelcontextprotocol 获取最新动态。

7.6 RAG 基础

RAG(Retrieval-Augmented Generation,检索增强生成)是解决 LLM "幻觉"和"知识过时"问题的核心技术。

为什么需要 RAG

LLM 有两个固有缺陷:

  1. 幻觉:模型会"编造"看似合理但实际错误的信息
  2. 知识截止:训练数据有截止日期,无法获取最新信息

RAG 的思路是:在生成回答前,先从外部知识库检索相关信息,把检索结果作为上下文提供给模型,让模型"有据可依"。

ℹ️前端类比

RAG 就像前端的数据获取流程:组件渲染前,先调用 API 获取数据(检索),再把数据传入组件渲染(生成)。没有数据获取的组件只能显示静态内容(类似 LLM 的预训练知识)。

Embedding 与向量

Embedding 是将文本转换为数值向量的技术。语义相近的文本,其向量在空间中距离也更近。

python
from langchain_openai import OpenAIEmbeddings

# 初始化 Embedding 模型
embeddings = OpenAIEmbeddings(
    model="text-embedding-3-small",
    api_key="sk-your-key"
)

# 将文本转为向量
texts = [
    "Python 是一种解释型编程语言",
    "JavaScript 是前端开发的核心语言",
    "Python 适合数据分析和人工智能"
]

vectors = embeddings.embed_documents(texts)
print(f"文本数量: {len(vectors)}")
print(f"每个向量维度: {len(vectors[0])}")
# 输出: 文本数量: 3, 每个向量维度: 1536

# 计算相似度(余弦相似度)
import numpy as np

def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

# "Python 数据分析" 与第一个文本更相关
query = embeddings.embed_query("Python 数据分析")
sim1 = cosine_similarity(query, vectors[0])  # ~0.85
sim2 = cosine_similarity(query, vectors[1])  # ~0.45
print(f"与'Python 语言'相似度: {sim1:.3f}")
print(f"与'JavaScript'相似度: {sim2:.3f}")

文本分割

长文档需要分割成小块(Chunk)才能存入向量数据库。分割策略直接影响检索质量:

python
from langchain.text_splitter import (
    RecursiveCharacterTextSplitter,
    CharacterTextSplitter
)

# 1. 递归字符分割器(推荐)
# 按优先级尝试分割:段落 -> 句子 -> 单词 -> 字符
recursive_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,      # 每块最大 500 字符
    chunk_overlap=50,    # 块之间重叠 50 字符(保持上下文连贯)
    separators=["\n\n", "\n", "。", "!", "?", " ", ""]
)

# 2. 固定字符分割器
char_splitter = CharacterTextSplitter(
    separator="\n",
    chunk_size=1000,
    chunk_overlap=200
)

# 使用示例
long_text = """Python 是由 Guido van Rossum 于 1991 年创建的...
(此处省略一篇长文章)"""

chunks = recursive_splitter.split_text(long_text)
print(f"分割为 {len(chunks)} 个块")
for i, chunk in enumerate(chunks[:3]):
    print(f"块 {i}: {chunk[:50]}...")

ChromaDB 向量数据库

ChromaDB 是轻量级的开源向量数据库,适合入门和中小型项目。

bash
pip install chromadb langchain-chroma
python
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings

# 初始化 Embedding 和向量存储
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# 创建/加载向量数据库
vectorstore = Chroma(
    collection_name="python_tutorial",
    embedding_function=embeddings,
    persist_directory="./chroma_db"  # 数据持久化目录
)

# 添加文档
from langchain_core.documents import Document

docs = [
    Document(page_content="Python 的列表推导式是一种简洁创建列表的方式。", metadata={"source": "ch2"}),
    Document(page_content="装饰器是 Python 中修改函数行为的高级特性。", metadata={"source": "ch3"}),
    Document(page_content="asyncio 是 Python 的异步编程库。", metadata={"source": "ch4"})
]

vectorstore.add_documents(docs)

# 相似性搜索
results = vectorstore.similarity_search("如何创建列表?", k=2)
for doc in results:
    print(f"内容: {doc.page_content}")
    print(f"来源: {doc.metadata['source']}")
    print("---")

# 带分数的搜索(分数越低越相似)
results_with_scores = vectorstore.similarity_search_with_score("异步编程", k=2)
for doc, score in results_with_scores:
    print(f"分数: {score:.4f}, 内容: {doc.page_content}")

RAG 完整检索流程

python
from langchain.chains import RetrievalQA
from langchain_openai import ChatOpenAI

# 创建检索器
retriever = vectorstore.as_retriever(
    search_type="similarity",  # 相似度搜索
    search_kwargs={"k": 3}     # 返回前 3 个结果
)

# 创建 LLM
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# 组装 RAG Chain
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",  # 将所有检索结果"塞"进 Prompt
    retriever=retriever,
    return_source_documents=True,  # 返回来源文档
    verbose=True
)

# 提问
result = qa_chain.invoke({"query": "Python 装饰器是什么?"})
print(f"回答: {result['result']}")
print("\n参考来源:")
for doc in result["source_documents"]:
    print(f"  - {doc.page_content[:60]}...")

7.7 实战:个人知识库问答系统

综合运用 LangChain + ChromaDB,构建一个能读取本地文档并回答问题的知识库系统。

python
#!/usr/bin/env python3
"""
knowledge_base_qa.py
基于 LangChain + ChromaDB 的个人知识库问答系统

功能:
1. 加载本地文档(txt, md, pdf)
2. 自动分割、Embedding、存入向量库
3. 支持自然语言查询
"""

import os
import sys
from pathlib import Path
from dotenv import load_dotenv

from langchain_community.document_loaders import (
    TextLoader,
    UnstructuredMarkdownLoader,
    DirectoryLoader
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationBufferMemory

load_dotenv()

class KnowledgeBaseQA:
    def __init__(self, docs_dir="./docs", db_dir="./chroma_db"):
        self.docs_dir = Path(docs_dir)
        self.db_dir = Path(db_dir)

        # 初始化 Embedding
        self.embeddings = OpenAIEmbeddings(
            model="text-embedding-3-small",
            api_key=os.getenv("OPENAI_API_KEY"),
            base_url=os.getenv("OPENAI_BASE_URL")
        )

        # 初始化 LLM
        self.llm = ChatOpenAI(
            model=os.getenv("LLM_MODEL", "gpt-4o-mini"),
            api_key=os.getenv("OPENAI_API_KEY"),
            base_url=os.getenv("OPENAI_BASE_URL"),
            temperature=0.3
        )

        self.vectorstore = None
        self.qa_chain = None

    def load_documents(self):
        """加载文档目录中的所有支持文件"""
        print(f"正在加载文档: {self.docs_dir}")

        documents = []

        # 加载 txt 文件
        if list(self.docs_dir.glob("*.txt")):
            txt_loader = DirectoryLoader(
                str(self.docs_dir),
                glob="*.txt",
                loader_cls=TextLoader,
                loader_kwargs={"encoding": "utf-8"}
            )
            documents.extend(txt_loader.load())

        # 加载 md 文件
        if list(self.docs_dir.glob("*.md")):
            md_loader = DirectoryLoader(
                str(self.docs_dir),
                glob="*.md",
                loader_cls=UnstructuredMarkdownLoader
            )
            documents.extend(md_loader.load())

        print(f"共加载 {len(documents)} 个文档")
        return documents

    def build_knowledge_base(self):
        """构建知识库:分割 -> Embedding -> 存储"""
        documents = self.load_documents()

        if not documents:
            print("警告: 没有找到任何文档")
            return False

        # 文本分割
        splitter = RecursiveCharacterTextSplitter(
            chunk_size=500,
            chunk_overlap=50,
            separators=["\n\n", "\n", "。", "!", "?", " ", ""]
        )
        chunks = splitter.split_documents(documents)
        print(f"分割为 {len(chunks)} 个文本块")

        # 存入向量数据库
        self.vectorstore = Chroma.from_documents(
            documents=chunks,
            embedding=self.embeddings,
            persist_directory=str(self.db_dir)
        )
        print(f"知识库已保存到: {self.db_dir}")
        return True

    def load_existing_kb(self):
        """加载已存在的知识库"""
        if not self.db_dir.exists():
            return False

        self.vectorstore = Chroma(
            persist_directory=str(self.db_dir),
            embedding_function=self.embeddings
        )
        print("已加载现有知识库")
        return True

    def create_qa_chain(self):
        """创建问答链"""
        if not self.vectorstore:
            raise ValueError("知识库未初始化")

        # 对话记忆
        memory = ConversationBufferMemory(
            memory_key="chat_history",
            return_messages=True,
            output_key="answer"
        )

        # 检索器
        retriever = self.vectorstore.as_retriever(
            search_type="similarity",
            search_kwargs={"k": 4}
        )

        # 对话式 RAG 链
        self.qa_chain = ConversationalRetrievalChain.from_llm(
            llm=self.llm,
            retriever=retriever,
            memory=memory,
            return_source_documents=True,
            verbose=False
        )

    def ask(self, question):
        """提问"""
        if not self.qa_chain:
            raise ValueError("问答链未初始化")

        result = self.qa_chain.invoke({"question": question})
        return {
            "answer": result["answer"],
            "sources": [doc.page_content[:100] + "..."
                       for doc in result.get("source_documents", [])]
        }

    def run_interactive(self):
        """交互式问答"""
        print("\n" + "=" * 50)
        print("  个人知识库问答系统")
        print("  输入 /quit 退出,/rebuild 重建知识库")
        print("=" * 50 + "\n")

        # 尝试加载或构建知识库
        if not self.load_existing_kb():
            if self.docs_dir.exists():
                self.build_knowledge_base()
            else:
                print(f"请先在 {self.docs_dir} 目录放入文档")
                return

        self.create_qa_chain()

        while True:
            question = input("问题: ").strip()
            if question == "/quit":
                break
            if question == "/rebuild":
                self.build_knowledge_base()
                self.create_qa_chain()
                continue
            if not question:
                continue

            try:
                result = self.ask(question)
                print(f"\n回答: {result['answer']}\n")
                if result["sources"]:
                    print("参考片段:")
                    for i, src in enumerate(result["sources"][:2], 1):
                        print(f"  {i}. {src}")
                print()
            except Exception as e:
                print(f"错误: {e}\n")


def main():
    # 创建示例文档目录
    docs_dir = Path("./docs")
    docs_dir.mkdir(exist_ok=True)

    # 创建示例文档
    sample_file = docs_dir / "python_notes.txt"
    if not sample_file.exists():
        sample_file.write_text("""Python 学习笔记

列表推导式:
列表推导式是 Python 中创建列表的简洁方式。
语法:[expression for item in iterable if condition]
示例:squares = [x**2 for x in range(10)]

装饰器:
装饰器是一种修改函数行为的高级特性。
使用 @ 语法糖,本质上是一个接收函数并返回函数的函数。
常用于日志记录、权限检查、缓存等场景。

虚拟环境:
Python 使用 venv 模块创建隔离的开发环境。
命令:python -m venv myenv
激活:source myenv/bin/activate (Linux/Mac)
      myenv\\Scripts\\activate (Windows)
""", encoding="utf-8")
        print(f"已创建示例文档: {sample_file}")

    # 启动系统
    kb = KnowledgeBaseQA(docs_dir="./docs", db_dir="./chroma_db")
    kb.run_interactive()


if __name__ == "__main__":
    main()
💡扩展方向

在此基础上,你可以添加:多格式支持(PDF、Word)、增量更新(只处理新文件)、Web 界面(Gradio/Streamlit)、混合检索(关键词 + 向量)、重排序(Rerank)等高级功能。

7.8 LangGraph —— 状态机工作流

LangGraph 是 LangChain 团队推出的库,用于构建有状态的、多角色的 Agent 工作流。 如果说 LangChain 的 Chain 是"管道"(数据单向流动),LangGraph 就是"状态机"(数据可以在节点间循环、分支)。 这非常像前端的 Redux 或 XState 的概念——中心化的状态 + 状态转换图。

ℹ️前端类比

LangChain Chain = 数据管道(pipe/waterfall);LangGraph = 状态机(Redux store + reducer)。 当你的 Agent 需要在不同策略间切换、循环执行直到满足条件时,LangGraph 是更好的选择。

python
"""LangGraph 示例 —— 带条件判断的 Agent 工作流 (pip install langgraph)"""
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, END

# 定义状态(类似 Redux store 的 state 类型)
class AgentState(TypedDict):
    messages: list[str]
    next_action: str
    result: str | None

# 定义节点(每个节点是一个处理步骤)
def analyze_task(state: AgentState) -> AgentState:
    """分析任务,决定下一步"""
    last_msg = state["messages"][-1] if state["messages"] else ""
    if "计算" in last_msg:
        return {**state, "next_action": "calculate"}
    elif "翻译" in last_msg:
        return {**state, "next_action": "translate"}
    else:
        return {**state, "next_action": "answer"}

def calculate(state: AgentState) -> AgentState:
    """执行计算"""
    state["result"] = "计算结果: 42"
    return state

def translate(state: AgentState) -> AgentState:
    """执行翻译"""
    state["result"] = "翻译结果: Hello World"
    return state

def direct_answer(state: AgentState) -> AgentState:
    """直接回答"""
    state["result"] = "这是一个通用问题的回答"
    return state

# 路由函数:根据状态决定下一步
def router(state: AgentState) -> str:
    return state["next_action"]

# 构建图
graph = StateGraph(AgentState)

# 添加节点
graph.add_node("analyze", analyze_task)
graph.add_node("calculate", calculate)
graph.add_node("translate", translate)
graph.add_node("direct_answer", direct_answer)

# 添加边
graph.set_entry_point("analyze")  # 入口
graph.add_conditional_edges("analyze", router, {
    "calculate": "calculate",
    "translate": "translate",
    "answer": "direct_answer"
})
graph.add_edge("calculate", END)   # 终止
graph.add_edge("translate", END)
graph.add_edge("direct_answer", END)

# 编译并运行
app = graph.compile()
result = app.invoke({"messages": ["帮我计算 2+2"], "next_action": "", "result": None})
print(result["result"])  # 计算结果: 42

7.9 LlamaIndex 简介

LlamaIndex 是与 LangChain 互补的框架,专注于数据索引和检索。 如果说 LangChain 是"AI 应用的 React",LlamaIndex 就是"AI 数据的数据库引擎"。 它在 RAG(检索增强生成)场景中特别强大。

python
"""LlamaIndex 示例 —— 快速构建 RAG 应用 (pip install llama-index)"""
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader

# 1. 加载文档(支持 PDF、Word、Markdown 等格式)
documents = SimpleDirectoryReader("./docs").load_data()

# 2. 自动分块、Embedding、索引(一行代码!)
index = VectorStoreIndex.from_documents(documents)

# 3. 创建查询引擎
query_engine = index.as_query_engine(
    similarity_top_k=3,  # 检索前 3 个最相关的文档块
    streaming=False
)

# 4. 提问
response = query_engine.query("这个项目使用什么数据库?")
print(response)

# LlamaIndex 的优势:
# - 内置多种文档加载器(PDF、Word、Notion、GitHub 等)
# - 多种检索策略(关键词 + 向量混合检索)
# - 支持结构化数据查询(结合 SQL)
# - 内置评估框架
ℹ️LangChain vs LlamaIndex 选择建议
场景推荐
构建对话 Agent(多工具、多步骤)LangChain + LangGraph
文档问答 / 知识库(核心是检索)LlamaIndex
复杂 AI 应用(两者都用到)两者结合使用
快速原型(RAG 应用)LlamaIndex(更简单)

7.10 Agent 评估方法

Agent 开发的最大挑战之一是评估——你怎么知道 Agent 的回复质量是好是坏? 以下是一些实用的评估策略:

LLM-as-Judge 评估

用另一个 LLM 来评估 Agent 的输出,就像代码审查一样:

python
"""LLM-as-Judge 评估示例"""
def evaluate_response(question, answer, expected_keywords, model="gpt-4o-mini"):
    """用 LLM 评估 Agent 回答质量"""

    evaluation_prompt = f"""请评估以下 AI 助手的回答质量:

用户问题: {question}
AI 回答: {answer}
期望包含的关键信息: {", ".join(expected_keywords)}

请按以下维度打分(1-5):
1. 准确性:回答是否包含正确的关键信息?
2. 完整性:回答是否覆盖了用户的全部问题?
3. 清晰度:回答是否易于理解?
4. 安全性:回答是否有潜在的安全风险?

以 JSON 格式输出:
{{"accuracy": int, "completeness": int, "clarity": int, "safety": int, "overall": float, "comments": str}}"""

    response = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": evaluation_prompt}],
        response_format={"type": "json_object"}
    )

    return json.loads(response.choices[0].message.content)

# 构建测试用例集
test_cases = [
    {"question": "如何安全地解析用户输入?", "expected_keywords": ["验证", "sanitize", "SQL注入", "XSS"]},
    {"question": "什么是列表推导式?", "expected_keywords": ["列表", "推导式", "简洁", "for", "if"]},
]

for case in test_cases:
    answer = your_agent.ask(case["question"])
    score = evaluate_response(case["question"], answer, case["expected_keywords"])
    print(f"Q: {case['question'][:30]}...")
    print(f"Score: {score['overall']}/5")
    print(f"Comments: {score['comments']}")
    print("---")
💡评估最佳实践
  1. 准备测试集:收集 50-200 个真实用户问题作为评估基准
  2. 人工标注:初期人工评估 10-20 个回答,形成质量认知
  3. 自动化回归:每次修改 Prompt 或工具后,自动跑评估集
  4. 关注"坏"案例:失败案例比成功案例更有价值
  5. 使用 LangSmith / Langfuse:专业的 LLM 可观测性平台,记录和评估每次调用