大模型应用实战:如何解决大模型开发中的上下文窗口与成本难题?

部署运行你感兴趣的模型镜像

大模型应用实战(一):如何解决大模型开发中的上下文窗口与成本难题?

你是否曾满怀激情地调用大模型API,却发现它像金鱼一样,只有七秒记忆,刚刚才告诉它的事情,转头就忘得一干二净?或者,你只是跑了几个测试,月底收到账单时,才发现数字高得令人心跳加速?

如果这些场景让你感同身受,那么恭喜你,你已经踩过了每个大模型开发者必经的两个“大坑”:恼人的上下文窗口(Context Window)限制和高昂的API调用成本

别担心,这并不是你一个人的困扰。这两个问题,是横亘在无数创意和落地应用之间的两座大山。而本系列教程的目的,就是带你一起,亲手凿穿它们。我们将摒弃空洞的理论,从零开始,手把手地带你构建一个完整的项目。在这个过程中,你不仅会理解问题的本质,更将学会一套行之有效的解决方案,一套能帮你“省钱又高效”的开发心法。

准备好了吗?让我们一起,从最基础的概念开始,踏出坚实的第一步。

第一部分:拨开迷雾 —— 理解两大核心挑战

在动手写代码之前,我们必须先弄清楚我们的“敌人”究竟是谁。

1. 什么是“上下文窗口”(Context Window)?

你可以把大模型的“上下文窗口”想象成它的短期记忆。当你和模型进行一次对话(或者给它一段文本让它处理)时,它并不是真的“理解”了全部内容,而只是在它有限的记忆范围内进行分析和回应。

这个记忆的容量,就是由“上下文窗口”的大小决定的。一旦你输入的信息超出了这个窗口,模型就会像一个记性不好的朋友,忘记掉最开始的部分。

这个容量是如何计算的呢?答案是 Token

Token是模型处理文本的最小单位。对于英文来说,一个单词约等于1.3个Token;对于中文,一个汉字大约是2个Token。例如,一个拥有4096 Token上下文窗口的模型,意味着它一次最多只能“记住”大约3000个英文单词或2000个汉字的内容。

实际影响是什么?
假设你想让模型总结一本长达数万字的小说。如果你简单地将整本小说的内容直接丢给它,模型很可能只会根据它能“看到”的最后几千字来给出总结,而忽略了开头的关键情节。这就是上下文窗口限制带来的最直接的问题——信息丢失

2. “API成本”是如何产生的?

天下没有免费的午餐,强大的模型背后是昂贵的计算资源。目前,主流的大模型服务商,如OpenAI,其API调用是按量计费的,计费单位同样是 Token

通常,成本分为两部分:

  • 输入(Prompt Tokens): 你发送给模型的文本所包含的Token数量。
  • 输出(Completion Tokens): 模型为你生成的回答所包含的Token数量。

这意味着,你和模型说的每一句话,它回复的每一个字,都在燃烧你的经费。在开发调试阶段,我们可能会频繁地发送请求,如果不加控制,即使是处理一些中等长度的文本,成本也会迅速累积。

核心矛盾
现在,我们面临的核心矛盾就显而易见了:

  • 我们希望模型能处理尽可能长的文本,拥有更全面的“记忆”,以完成复杂的任务(扩展上下文)。
  • 我们希望尽可能减少发送和接收的Token数量,以控制我们的项目预算(降低成本)。

这看似不可调和的矛盾,正是我们这篇教程要用技术和智慧去解决的。

第二部分:环境准备 —— 搭建我们的实验平台

理论讲完了,让我们开始动手实践。

1. 项目目标

我们将构建一个简单而实用的**“智能文档问答机器人”**。这个机器人可以读取你提供的一个本地文本文档,然后根据文档内容,回答你的任何提问。这个项目虽小,却五脏俱全,能让我们直面并解决前面提到的两大核心挑战。

2. 技术选型与理由

  • 编程语言:Python (3.8或更高版本)

    • 为什么? Python是目前人工智能和数据科学领域当之无愧的王者,拥有最庞大、最活跃的社区和最丰富的第三方库支持。我们接下来要用到的所有工具,几乎都是为Python优先打造的。
  • 核心框架:LangChain

    • 为什么? LangChain是一个强大的大模型应用开发框架。它将与大模型交互的许多复杂、重复的步骤(如文本分割、API调用、数据管理等)都封装成了简单易用的模块。使用它,我们可以不必从最底层的API请求开始写起,从而极大地提高开发效率。
  • 大模型API:OpenAI API

    • 为什么? OpenAI的GPT系列模型是当前行业的标杆,其API接口设计成熟、文档清晰,非常适合初学者入门。我们将使用它的模型来驱动我们的机器人。

3. 一步一脚印:环境搭建指南

让我们像搭建乐高一样,一步步构建起我们的开发环境。

第一步:创建并激活虚拟环境

为了不污染你电脑上全局的Python环境,也为了让我们的项目依赖清晰明了,使用虚拟环境是一个非常好的习惯。

打开你的终端(在Windows上是命令提示符或PowerShell,在macOS或Linux上是Terminal),进入你打算存放项目的文件夹,然后执行以下命令:

# 创建一个名为 "venv" 的虚拟环境文件夹
python -m venv venv

# 激活这个环境
# 在 macOS 或 Linux 上:
source venv/bin/activate
# 在 Windows 上:
.\venv\Scripts\activate

激活成功后,你会看到终端提示符前面出现 (venv) 的字样。这表示你现在所有的操作都将在这个独立的环境中进行。

第二步:安装必要的Python库

在激活的虚拟环境中,执行以下命令来安装我们项目所需的所有第三方库:

pip install openai langchain python-dotenv chromadb tiktoken

我们来解释一下每个库的作用:

  • openai: OpenAI官方提供的Python库,是与GPT模型API交互的桥梁。
  • langchain: 我们选择的核心开发框架,用于组织和管理整个应用流程。
  • python-dotenv: 一个实用的小工具,可以帮助我们从一个名为.env的文件中加载环境变量,这样我们就不需要把敏感的API密钥直接写在代码里。
  • chromadb: 一个开源的、轻量级的向量数据库。它将成为我们机器人的“外挂大脑”,用来存放文档的“记忆片段”。
  • tiktoken: OpenAI官方出品的Token计算工具。它能帮助我们在将文本发送给模型之前,就精确地计算出它会消耗多少Token,是成本控制的关键。

第三步:获取并配置API密钥

  1. 访问 OpenAI的官方网站 并注册一个账户。
  2. 登录后,在个人中心的“API keys”页面创建一个新的密钥。
  3. 重要: 这个密钥只会出现一次,请立即复制并妥善保管。不要泄露给任何人,也不要直接上传到GitHub等公开代码库中。

第四步:初始化我们的项目结构

现在,让我们把项目的文件结构建立起来。

  1. 在你选择的工作目录下,创建一个新的文件夹,命名为 llm_cost_saver
  2. 进入这个文件夹,创建两个文件:
    • 一个名为 .env 的文件。
    • 一个名为 main.py 的文件。

打开 .env 文件,在里面写下如下内容,并将sk-xxxxxxxxxx替换为你刚刚复制的OpenAI API密钥:

OPENAI_API_KEY="sk-xxxxxxxxxx"

这个.env文件专门用来存放我们的密钥等敏感信息。通过python-dotenv库,我们的Python代码可以自动读取这个文件,既方便又安全。

main.py将是我们的主战场,所有的代码逻辑都将在这里编写。

至此,我们的准备工作全部完成!一个干净、独立、配置齐全的开发环境已经就绪。下一部分,我们将正式进入核心技术的实战环节。

第三部分:核心技术实战 —— RAG(检索增强生成)

还记得我们提到的上下文窗口限制吗?如果我们想让模型处理一篇长文档,直接把全文丢过去是行不通的。那么,该怎么办呢?答案就是使用一种名为RAG (Retrieval-Augmented Generation,检索增强生成) 的技术。

1. 解决上下文窗口的“魔法”—— RAG是什么?

RAG的核心思想非常直观:既然模型的“短期记忆”有限,那我们就不把所有信息都硬塞给它。取而代之,我们给它建立一个外部的、可以随时查询的“知识库”(就像一个外挂大脑)。

当用户提出一个问题时,我们不再直接把问题丢给模型,而是先进行一步“预处理”:

  1. 检索(Retrieval): 首先,我们拿着用户的问题,去我们的“知识库”里,找到与问题最相关的几段信息。
  2. 增强(Augmented): 然后,我们将这些检索到的相关信息,连同用户的原始问题一起,打包成一个新的、信息更丰富的提示(Prompt)。
  3. 生成(Generation): 最后,我们把这个“增强”后的提示发送给大模型。此时,模型看到的不再是一个孤零零的问题,而是问题以及解决这个问题所需的所有背景资料。它只需要根据这些现成的资料,进行总结和回答即可。

通过这种方式,无论原始文档有多长,我们每次喂给模型的都只是与当前问题最相关的几个“片段”。这不仅完美绕过了上下文窗口的限制,还极大地提高了回答的准确性,并显著降低了API调用的Token消耗。

下面是RAG的工作流程图,能帮助你更清晰地理解整个过程:

graph TD
    subgraph "准备阶段 (数据预处理)"
        G[你的原始长文档] --> H{文本分割器};
        H -- 切分成小块 --> I[文本片段 Chunks];
        I --> J{Embedding模型};
        J -- 转化为向量 --> K[向量数据];
        K --> L[存入向量数据库];
    end

    subgraph "问答阶段 (实时查询)"
        A[用户提出问题] --> B{Embedding模型};
        B -- 同样转化为向量 --> C[问题向量];
        C --> D{检索模块};
        L -- 供检索 --> D;
        D -- 在数据库中查找最相似的向量 --> E[相关的文档片段];
        E --> F[Prompt拼接模块];
        A -- 原始问题 --> F;
        F -- 拼接成最终Prompt --> H_LLM{大语言模型 (LLM)};
        H_LLM --> I_Final[生成最终答案];
    end

2. 代码实战:从零构建RAG流程

现在,让我们打开main.py文件,用代码将上面的流程一步步实现。

准备工作:创建一个示例文档

llm_cost_saver文件夹中,创建一个名为my_document.txt的文本文件,并粘贴以下内容:

项目背景:
“奥德赛计划”是一项旨在探索A-7行星系中潜在生命形式的长期太空探索项目。该计划由全球航天联合会(GSA)于2042年启动,核心任务是利用“探索者四号”无人探测器对该星系的三颗主要行星(阿尔法、贝塔、伽马)进行详细勘察。

技术细节:
“探索者四号”探测器搭载了最新的光谱分析仪、地质采样钻和高分辨率成像系统。其动力来源是微型核聚变反应堆,理论上可以支持长达50年的持续运作。探测器通过超光速量子纠缠通信技术与地球指挥中心保持联系,数据传输延迟仅为数分钟。

项目进展:
目前,“探索者四号”已成功抵达A-7星系,并完成了对阿尔法星的初步探测。数据显示,阿尔法星大气中含有微量的甲烷和氧气,这为存在微生物生命提供了可能性,但尚无直接证据。下一步,探测器将前往贝塔星,预计航程为六个月。贝塔星地表被厚厚的冰层覆盖,科学家猜测冰层下可能存在液态水海洋。伽马星由于轨道不稳定,暂被列为低优先级目标。

这个文档将成为我们机器人的知识库。

第一步:加载并配置环境

main.py的顶部,我们先导入必要的库,并加载.env文件中的API密钥。

import os
from dotenv import load_dotenv

# 导入LangChain相关的库
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings, OpenAI

# 加载 .env 文件中的环境变量
# 这行代码会查找当前目录下的 .env 文件,并把里面定义的变量加载到系统的环境变量中
load_dotenv()

# 从环境变量中获取 OpenAI API Key
# 这样做的好处是避免将敏感信息硬编码在代码里
api_key = os.getenv("OPENAI_API_KEY")

# 检查 API Key 是否成功加载
if not api_key:
    raise ValueError("未找到 OpenAI API Key,请检查你的 .env 文件。")

print("环境配置成功!")

# 定义我们将要使用的文档路径
DOCUMENT_PATH = "my_document.txt"

第二步:加载与分割文档

我们需要把my_document.txt读入程序,并将其切分成更小的、易于处理的块(Chunks)。

# ... (接上面的代码)

def split_document(doc_path):
    """
    加载并分割指定的文本文档。
    :param doc_path: 文档的路径。
    :return: 分割后的文本块列表。
    """
    print(f"正在加载文档: {doc_path}")
    # 1. 加载文档
    # TextLoader 是 LangChain 提供的一个工具,专门用来加载纯文本文档。
    loader = TextLoader(doc_path, encoding='utf-8')
    documents = loader.load()

    # 2. 分割文档
    # RecursiveCharacterTextSplitter 是一个智能的文本分割器。
    # 它会尝试按段落、句子、单词等顺序来分割文本,以保持语义的完整性。
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=200,      # 每个块的目标大小(字符数)。可以根据文档内容和模型上下文窗口调整。
        chunk_overlap=20,    # 相邻块之间的重叠字符数。这有助于在块的边界处保持上下文的连续性。
        length_function=len, # 使用 Python 内置的 len 函数来计算文本长度。
    )
    splitted_docs = text_splitter.split_documents(documents)
    
    print(f"文档分割完成,共得到 {len(splitted_docs)} 个文本块。")
    return splitted_docs

# 执行分割
document_chunks = split_document(DOCUMENT_PATH)

第三步:向量化与存储

接下来,我们需要将这些文本块“翻译”成模型能理解的数学语言——向量(Embeddings),并存入我们的向量数据库Chroma中。

# ... (接上面的代码)

def create_vector_store(docs, embeddings):
    """
    根据文档块和指定的 embedding 模型,创建并持久化一个向量数据库。
    :param docs: 分割后的文档块列表。
    :param embeddings: 用于生成向量的 embedding 模型实例。
    :return: 创建好的向量数据库实例。
    """
    print("正在创建向量数据库...")
    # 使用 Chroma.from_documents 方法可以一步到位:
    # 1. 为每个文档块生成 embedding。
    # 2. 将文档和其对应的 embedding 存入 Chroma 数据库中。
    # persist_directory 参数指定了数据库在磁盘上的存储位置。
    vector_store = Chroma.from_documents(
        documents=docs, 
        embedding=embeddings
    )
    print("向量数据库创建成功!")
    return vector_store

# 实例化 OpenAI 的 embedding 模型
# 这个模型专门负责将文本转换成高维向量
openai_embeddings = OpenAIEmbeddings(openai_api_key=api_key)

# 创建向量数据库
vector_store = create_vector_store(document_chunks, openai_embeddings)

第四步:创建检索器并发起提问

万事俱备!现在我们可以创建一个检索器(Retriever),它能根据我们的问题,从向量数据库中找出最相关的文档片段。

# ... (接上面的代码)

# 实例化一个大语言模型(LLM)
# 我们在这里使用 OpenAI 的基础模型,它在成本和性能之间取得了很好的平衡
llm = OpenAI(openai_api_key=api_key, temperature=0) # temperature=0 表示我们希望模型给出更确定性的、而非创造性的回答

# 从向量数据库创建一个检索器
# .as_retriever() 是一个便捷方法,它将向量数据库转换为一个标准的 LangChain Retriever 接口。
retriever = vector_store.as_retriever()

# 使用 LangChain 的 RetrievalQA 链
# 这个“链”将检索和问答两个步骤优雅地串联了起来。
from langchain.chains import RetrievalQA

qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff", # "stuff" 是最简单的链类型,它会把检索到的所有文档片段“塞”进一个 Prompt 中。
    retriever=retriever
)

# 开始提问!
question1 = "“奥德赛计划”的目标是什么?"
answer1 = qa_chain.invoke(question1)
print(f"问题: {question1}")
print(f"答案: {answer1['result']}\n")

question2 = "探测器使用什么技术与地球通信?"
answer2 = qa_chain.invoke(question2)
print(f"问题: {question2}")
print(f"答案: {answer2['result']}\n")

question3 = "探测器下一步要去哪个星球?为什么?"
answer3 = qa_chain.invoke(question3)
print(f"问题: {question3}")
print(f"答案: {answer3['result']}\n")

现在,将以上所有代码片段整合到main.py文件中,然后在终端中运行它:
python main.py

你应该能看到,程序会准确地根据my_document.txt的内容回答你的问题,而它并没有将整个文档都发送给OpenAI。我们成功了!我们用RAG技术,漂亮地解决了上下文窗口的难题。

第四部分:成本控制的艺术 —— 做一个精明的开发者

解决了上下文问题,我们再来看看另一个核心——成本。虽然RAG本身已经通过减少发送的文本量大大节约了成本,但我们还可以做得更好。

策略一:选择合适的模型

OpenAI提供了多种不同能力的模型,它们的价格差异巨大。例如,GPT-4的功能强大,但价格昂贵;而GPT-3.5-Turbo等模型,在处理许多常规任务时已经绰绰有余,价格却便宜得多。

核心原则:杀鸡焉用牛刀。

在LangChain中切换模型非常简单。你只需要在实例化LLM时,指定model_name即可。例如,如果你想使用成本更低的gpt-3.5-turbo-instruct模型(一个针对指令优化的GPT-3.5模型),可以这样修改代码:

# 将
# llm = OpenAI(openai_api_key=api_key, temperature=0)
# 修改为
llm = OpenAI(model_name="gpt-3.5-turbo-instruct", openai_api_key=api_key, temperature=0)

仅仅是这一行代码的改动,就可能让你的API成本降低90%以上,而回答质量在很多场景下并无明显下降。

策略二:精确的Token计算

要想控制成本,首先要能度量成本。tiktoken库就是我们的“成本计算器”。我们可以在发送API请求之前,就精确地知道将要花费多少Token。

让我们来编写一个简单的函数来计算Token数量:

import tiktoken

def count_tokens(text: str, model: str = "gpt-3.5-turbo") -> int:
    """
    计算给定文本在特定模型下会产生的Token数量。
    :param text: 需要计算的文本。
    :param model: 目标模型的名称。
    :return: Token数量。
    """
    try:
        # 获取对应模型的编码器
        encoding = tiktoken.encoding_for_model(model)
    except KeyError:
        # 如果模型没有特定的编码器,使用一个通用的
        encoding = tiktoken.get_encoding("cl100k_base")
    
    # 对文本进行编码,并计算编码后的token数量
    num_tokens = len(encoding.encode(text))
    return num_tokens

# 示例:
my_prompt = "“奥德赛计划”的目标是什么?"
token_count = count_tokens(my_prompt)
print(f"'{my_prompt}' 这段文本的Token数量是: {token_count}")

这个函数非常有用。在复杂的应用中,你可以在每次调用LLM之前都用它来监控Token消耗,当消耗即将超出预算时,可以进行报警或中断操作。

策略三:优化Prompt

发送给模型的Prompt,是我们成本的直接来源之一。因此,言简意赅是优化Prompt的核心原则。

  • 避免冗余: 删除不必要的寒暄、背景描述和重复的指令。
  • 使用清晰的指令: 直接告诉模型要做什么,而不是让它去猜测。

示例对比:

啰嗦的Prompt (高成本):

“你好,GPT。我现在正在处理一个关于太空探索的项目文档,文档里提到了一个叫‘奥德赛计划’的东西。我希望你能仔细阅读我接下来提供给你的相关资料,然后帮我总结一下,这个所谓的‘奥德赛计划’,它最主要的目标和任务究竟是什么呢?请用简洁的语言告诉我。”

精炼的Prompt (低成本):

“根据以下资料,总结‘奥德赛计划’的目标:\n[此处插入检索到的资料]”

通过对比两者的Token数量,你会发现精炼后的Prompt能为你省下可观的费用,尤其是在高频调用的场景下。

策略四:巧妙利用检索结果

在我们的RAG流程中,从向量数据库中检索回多少文档片段,也是一个可以优化的参数。

在创建retriever时,我们可以通过search_kwargs来控制检索的数量:

# k=2 表示我们每次只从数据库中检索出最相关的2个文档片段
retriever = vector_store.as_retriever(search_kwargs={"k": 2})

k值的选择需要权衡:

  • k值太小: 可能导致提供给模型的上下文信息不足,无法准确回答问题。
  • k值太大: 会增加Prompt的Token数量,从而增加API成本,甚至可能因为信息过多、掺入噪声而干扰模型的判断。

通常,从k=3k=4开始试验,是一个不错的选择。你可以根据实际应用的效果来动态调整这个值。

好的,我们继续这个系列。在上一篇文章中,我们成功构建了一个能够理解长文档并回答问题的“智能文档问答机器人”,解决了上下文窗口与初步的成本问题。现在,让我们来解决下一个关键挑战:让我们的机器人拥有记忆

大模型应用实战(二):如何为你的知识库机器人添加对话记忆?

在上一篇教程中,我们从零开始,成功构建了一个能够基于本地文档回答问题的“智能文档问答机器人”。它很棒,因为它能“读懂”长篇大论,但它也有个明显的缺点:它是个“健忘症患者”。

你和它的每一次互动都是一次全新的开始。你无法问它一个跟进的问题,因为它根本不记得你们上一秒聊了些什么。比如,你尝试这样和它对话:

你: “奥德赛计划的目标是什么?”
机器人: (基于文档给出了准确的回答…)
你: “那它是由哪个组织启动的?”
机器人: “对不起,我不理解‘它’指的是什么。”

看到问题了吗?这种“一问一答”式的交互,离我们想要的流畅“对话”还相去甚远。

在本篇实战教程中,我们将为我们的机器人进行一次关键的升级——植入“对话记忆”(Conversational Memory)。我们将让它告别健忘,学会联系上下文,真正理解我们的意图,从一个“问答工具”蜕变为一个真正的“对话伙伴”。

准备好给你的造物赋予记忆了吗?让我们开始吧。

第一部分:为什么“记忆”如此重要?

从“问答”到“对话”,看似一词之差,实则是一次质的飞跃。

单一的问答(Q&A)是无状态的(Stateless)。每一次请求都是完全独立的,就像在搜索引擎里一次又一次地输入查询。我们上一篇文章中使用的RetrievalQA链就是这种模式。它非常适合一次性的信息查询,但无法处理连续的交流。

而真正的对话是有状态的(Stateful)。后续的话语往往依赖于之前的内容。代词(“它”、“那个”)、省略的成分(“下一步去哪?”)、话题的延续,都建立在一个共享的上下文之上。

为了跨越这道鸿沟,我们的应用必须具备“记忆”能力。在大型语言模型应用中,记忆模块的核心作用有两个:

  1. 存储对话历史: 这是最基本的功能。它会像聊天记录一样,把用户和机器人之间的每一轮对话都保存下来。
  2. 重构后续问题: 这是实现流畅对话的关键。当用户提出一个依赖上下文的模糊问题时(例如,“它的动力来源呢?”),记忆系统会和LLM协作,结合之前存储的对话历史,将这个问题“翻译”成一个独立的、信息完整的、可供检索系统使用的新问题。比如,它会自动重构为:“‘探索者四号’探测器的动力来源是什么?”。

有了这个重构后的清晰问题,我们上一章构建的RAG系统才能准确地在向量数据库中找到相关信息,并给出正确的答案。

第二部分:LangChain中的“记忆”模块

幸运的是,强大的LangChain框架为我们提供了开箱即生的Memory组件。

它的设计理念,就是提供一个标准化的接口,来管理和操作对话的状态。开发者可以像插拔U盘一样,轻松地为自己的应用链(Chain)装上不同类型的“记忆芯片”。

虽然LangChain提供了多种记忆类型,但我们先了解几个最常见的:

  • ConversationBufferMemory: 这是最直接的一种记忆。它会把所有的对话历史原封不动地存储在一个变量里。它就像一个无限容量的记事本,简单、直观,非常适合我们的入门项目和不太长的对话场景。
  • ConversationBufferWindowMemory: 这是BufferMemory的升级版。它只保留最近的 k 轮对话。这就像一个只能记住最近几件事的朋友,好处是能防止对话历史变得过长,从而撑爆模型的上下文窗口,导致成本失控。
  • ConversationSummaryMemory: 当对话变得非常长时,前面两种方式都可能不太适用。这种记忆类型会调用一个LLM,在后台悄悄地把越来越长的对话历史总结成一段精炼的摘要。这是一种更高级、成本也更高的“记忆压缩”技术。

本次升级,我们将选用最基础也是最核心的 ConversationBufferMemory。它的原理足够我们理解记忆的工作机制,并且完全能满足当前的需求。

第三部分:代码升级 —— 植入记忆芯片

理论讲完,立刻开干!让我们回到上一篇创建的main.py文件,对其进行一次“大脑手术”。

1. 迎接新主角:ConversationalRetrievalChain

首先,我们需要认识到一个重要事实:我们之前使用的RetrievalQA链,其设计初衷就是为了无状态的问答,它本身并不支持接入记忆模块。

为了实现对话功能,我们需要请出一位新主角——ConversationalRetrievalChain。这个链是LangChain专门为“可对话的检索问答”场景量身打造的,它内建了处理对话历史和重构问题的逻辑。

它的内部工作流程如下图所示,这正是我们前面理论部分所描述的:

graph TD
    A[用户后续问题<br><i>(例如: "它的动力是什么?")</i>] --> B{ConversationalRetrievalChain};
    C[对话历史 (Memory)] --> B;
    B --> D[1. 调用LLM进行问题重构];
    D -- 生成 --> E[独立的、完整的新问题<br><i>(例如: "探索者四号的动力是什么?")</i>];
    E --> F{2. 检索模块 (Retriever)};
    G[向量数据库] --> F;
    F -- 查找相关文档 --> H[相关文档片段];
    E --> I{3. 调用LLM进行问答};
    H --> I;
    I -- 生成 --> J[最终答案];
    B -- 输出 --> J;
    J --> C; A --> C

2. 动手改造 main.py

现在,让我们对代码进行修改。请注意,改动的地方并不多,但每一步都很关键。

import os
from dotenv import load_dotenv

# --- 1. 导入新的模块 ---
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationBufferMemory
# --- (其他导入保持不变) ---
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings, OpenAI

# 加载环境变量
load_dotenv()
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
    raise ValueError("未找到 OpenAI API Key,请检查你的 .env 文件。")

# 定义文档路径
DOCUMENT_PATH = "my_document.txt"


def split_document(doc_path):
    """加载并分割指定的文本文档。"""
    print(f"正在加载文档: {doc_path}")
    loader = TextLoader(doc_path, encoding='utf-8')
    documents = loader.load()
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=200,
        chunk_overlap=20,
        length_function=len,
    )
    splitted_docs = text_splitter.split_documents(documents)
    print(f"文档分割完成,共得到 {len(splitted_docs)} 个文本块。")
    return splitted_docs


def create_vector_store(docs, embeddings):
    """根据文档块和 embedding 模型,创建向量数据库。"""
    print("正在创建向量数据库...")
    vector_store = Chroma.from_documents(documents=docs, embedding=embeddings)
    print("向量数据库创建成功!")
    return vector_store


# --- 主逻辑开始 ---

# 1. 分割文档
document_chunks = split_document(DOCUMENT_PATH)

# 2. 创建并持久化向量数据库
openai_embeddings = OpenAIEmbeddings(openai_api_key=api_key)
vector_store = create_vector_store(document_chunks, openai_embeddings)

# 3. 实例化 LLM
llm = OpenAI(model_name="gpt-3.5-turbo-instruct", openai_api_key=api_key, temperature=0)

# --- 2. 实例化 Memory ---
# 创建一个对话缓冲记忆实例
# memory_key="chat_history" 是告诉链(Chain),对话历史应该存储在哪个变量中。
# return_messages=True 表示记忆的输出将是消息对象列表,而不是简单的字符串。这是推荐的做法。
memory = ConversationBufferMemory(
    memory_key="chat_history", 
    return_messages=True
)

# --- 3. 创建带有记忆的对话检索链 ---
# 我们使用 ConversationalRetrievalChain.from_llm 来创建一个实例。
# 这个工厂方法为我们配置好了大部分默认设置。
qa_chain = ConversationalRetrievalChain.from_llm(
    llm=llm,
    retriever=vector_store.as_retriever(),
    memory=memory  # 将我们刚刚创建的 memory 实例关联到链上
)

# --- 4. 改造主逻辑为可持续对话的循环 ---
print("\n你好!我是你的文档问答机器人。输入 '退出' 来结束对话。")
chat_history = [] # 用于在循环外部手动跟踪历史,方便打印

while True:
    try:
        query = input("你: ")
        if query.lower() == '退出':
            print("机器人: 很高兴为您服务,再见!")
            break
        
        # 调用链来获取回答
        # 注意:现在调用链时需要传入问题和空的 chat_history
        # 链会自动从 memory 中读取并管理历史记录
        result = qa_chain.invoke({"question": query, "chat_history": chat_history})
        
        # 从结果中提取答案
        answer = result['answer']
        print(f"机器人: {answer}")

    except Exception as e:
        print(f"发生错误: {e}")
        break

代码改动解析:

  1. 导入新模块: 我们从langchain.chains导入了ConversationalRetrievalChain,并从langchain.memory导入了ConversationBufferMemory
  2. 实例化Memory: 我们创建了一个ConversationBufferMemory的实例。这是我们机器人的“记忆芯片”。
  3. 创建新Chain: 我们用ConversationalRetrievalChain.from_llm替换了原来的RetrievalQA。最关键的一步是,我们将memory实例传递给了它。这样,链和记忆就关联起来了。
  4. 改造为对话循环: 我们用一个while True:循环代替了原来单次的提问代码。这让我们可以和机器人进行连续的对话,直到我们输入“退出”。注意,在调用qa_chain.invoke时,虽然我们传入了一个空的chat_history,但链内部会自动从我们设置的memory对象中拉取并更新真正的对话历史。
第四部分:见证奇迹 —— 测试我们的新机器人

保存好你的main.py文件。现在,回到你的终端,像之前一样运行脚本:

python main.py

程序启动后,请你严格按照下面的顺序,来和你的机器人进行一次对话,亲身感受“记忆”的力量:

第一轮对话(建立初始上下文):

你: 奥德赛计划是做什么的?
机器人: (它应该会准确回答,这是关于探索A-7行星系潜在生命形式的太空项目…)

第二轮对话(测试代词指代):

你: 那它是由哪个组织启动的?
机器人: (见证奇迹的时刻!它现在应该能理解“它”指的就是“奥德赛计划”,并回答出“全球航天联合会(GSA)”。)

第三轮对话(测试省略主语):

你: 下一步要去哪里?
机器人: (它同样应该能联系上下文,理解你问的是“探测器”的下一步计划,并回答出将前往贝塔星。)

结果分析:
是不是感觉完全不同了?你的机器人不再是一个冷冰冰的查询工具,它开始能够“理解”你们之间的对话流了。每一次提问,ConversationalRetrievalChain都在后台默默地帮你把模糊的问题变得清晰,然后再去知识库里寻找答案。这背后的一切,都归功于我们刚刚植入的ConversationBufferMemory

您可能感兴趣的与本文相关的镜像

Python3.9

Python3.9

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

THMAIL

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值