Skip to content

第 7 章 · 高级文本生成技术与工具

本章目标:超越提示工程,使用 LangChain 组织 LLM 工作流——链式调用、多链编排、对话记忆与 ReAct 智能体。

7.1 准备工作

本章对应的官方笔记本是 chapter07/Chapter 7 - Advanced Text Generation Techniques and Tools.ipynb

💡 NOTE:运行本章示例需要 GPU。在 Google Colab 中,进入 Runtime > Change runtime type > Hardware accelerator > GPU > GPU type > T4

如果你在 Google Colab(或任何其他云环境)中查看笔记本,需要取消注释并运行以下代码块来安装本章依赖:

python
# %%capture
# !pip install langchain>=0.1.17 openai>=1.13.3 langchain_openai>=0.1.6 transformers>=4.40.1 datasets>=2.18.0 accelerate>=0.27.2 sentence-transformers>=2.5.1 duckduckgo-search>=5.2.2 langchain_community
# !CMAKE_ARGS="-DLLAMA_CUDA=on" pip install llama-cpp-python==0.2.69

7.2 Loading an LLM 加载语言模型

首先下载 Phi-3 mini 模型的 GGUF 量化权重:

python
!wget https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf/resolve/main/Phi-3-mini-4k-instruct-fp16.gguf

# If this command does not work for you, you can use the link directly to download the model
# https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf/resolve/main/Phi-3-mini-4k-instruct-fp16.gguf

通过 LangChain 的 LlamaCpp 封装加载模型:

python
from langchain import LlamaCpp

# Make sure the model path is correct for your system!
llm = LlamaCpp(
    model_path="Phi-3-mini-4k-instruct-fp16.gguf",
    n_gpu_layers=-1,
    max_tokens=500,
    n_ctx=2048,
    seed=42,
    verbose=False
)

测试一下基本问答:

python
llm.invoke("Hi! My name is Maarten. What is 1 + 1?")

7.3 Chains 链

链(Chain)把提示模板和 LLM 组合成一条可复用的流水线。先创建一个带 input_prompt 变量的提示模板:

python
from langchain import PromptTemplate

# Create a prompt template with the "input_prompt" variable
template = """<s><|user|>
{input_prompt}<|end|>
<|assistant|>"""
prompt = PromptTemplate(
    template=template,
    input_variables=["input_prompt"]
)

用管道符号 | 把提示与 LLM 连接成一条基础链:

python
basic_chain = prompt | llm
python
# Use the chain
basic_chain.invoke(
    {
        "input_prompt": "Hi! My name is Maarten. What is 1 + 1?",
    }
)

7.4 Multiple Chains 多条链协作

单个链的能力有限,真正的威力在于把多条链串联起来,让一条链的输出成为另一条链的输入。下面用一个「故事生成」例子演示三步流水线:起标题 → 描述主角 → 写故事。

第一步,创建负责生成故事标题的链:

python
from langchain import LLMChain

# Create a chain for the title of our story
template = """<s><|user|>
Create a title for a story about {summary}. Only return the title.<|end|>
<|assistant|>"""
title_prompt = PromptTemplate(template=template, input_variables=["summary"])
title = LLMChain(llm=llm, prompt=title_prompt, output_key="title")
python
title.invoke({"summary": "a girl that lost her mother"})

第二步,创建基于摘要与标题描述主角的链:

python
# Create a chain for the character description using the summary and title
template = """<s><|user|>
Describe the main character of a story about {summary} with the title {title}. Use only two sentences.<|end|>
<|assistant|>"""
character_prompt = PromptTemplate(
    template=template, input_variables=["summary", "title"]
)
character = LLMChain(llm=llm, prompt=character_prompt, output_key="character")

第三步,创建综合前三者输出完整故事的链:

python
# Create a chain for the story using the summary, title, and character description
template = """<s><|user|>
Create a story about {summary} with the title {title}. The main charachter is: {character}. Only return the story and it cannot be longer than one paragraph<|end|>
<|assistant|>"""
story_prompt = PromptTemplate(
    template=template, input_variables=["summary", "title", "character"]
)
story = LLMChain(llm=llm, prompt=story_prompt, output_key="story")

把三个组件串成完整流水线:

python
# Combine all three components to create the full chain
llm_chain = title | character | story
python
llm_chain.invoke("a girl that lost her mother")

7.5 Memory 记忆

LLM 天生是「无状态」的——每次调用都是独立的。验证一下:先告诉它名字,

python
# Let's give the LLM our name
basic_chain.invoke({"input_prompt": "Hi! My name is Maarten. What is 1 + 1?"})

再让它复述名字,它会失败:

python
# Next, we ask the LLM to reproduce the name
basic_chain.invoke({"input_prompt": "What is my name?"})

记忆(Memory)就是解决这个问题的机制。LangChain 提供了多种记忆策略。

7.5.1 ConversationBuffer 缓冲记忆

思路很直接:把聊天历史塞进提示里。先更新提示模板加入 chat_history 变量:

python
# Create an updated prompt template to include a chat history
template = """<s><|user|>Current conversation:{chat_history}

{input_prompt}<|end|>
<|assistant|>"""

prompt = PromptTemplate(
    template=template,
    input_variables=["input_prompt", "chat_history"]
)

引入 ConversationBufferMemory 并把它接入链中:

python
from langchain.memory import ConversationBufferMemory

# Define the type of Memory we will use
memory = ConversationBufferMemory(memory_key="chat_history")

# Chain the LLM, Prompt, and Memory together
llm_chain = LLMChain(
    prompt=prompt,
    llm=llm,
    memory=memory
)
python
# Generate a conversation and ask a basic question
llm_chain.invoke({"input_prompt": "Hi! My name is Maarten. What is 1 + 1?"})

现在 LLM 能记住我们说过的名字了:

python
# Does the LLM remember the name we gave it?
llm_chain.invoke({"input_prompt": "What is my name?"})

7.5.2 ConversationBufferMemoryWindow 窗口缓冲记忆

缓冲记忆的问题是对话越长提示越大。窗口记忆只保留最近 k 轮对话:

python
from langchain.memory import ConversationBufferWindowMemory

# Retain only the last 2 conversations in memory
memory = ConversationBufferWindowMemory(k=2, memory_key="chat_history")

# Chain the LLM, Prompt, and Memory together
llm_chain = LLMChain(
    prompt=prompt,
    llm=llm,
    memory=memory
)

进行两轮对话填充它的记忆窗口:

python
# Ask two questions and generate two conversations in its memory
llm_chain.invoke({"input_prompt":"Hi! My name is Maarten and I am 33 years old. What is 1 + 1?"})
llm_chain.invoke({"input_prompt":"What is 3 + 3?"})

它还记得名字吗?

python
# Check whether it knows the name we gave it
llm_chain.invoke({"input_prompt":"What is my name?"})

那年龄呢?(第一轮已被挤出窗口)

python
# Check whether it knows the age we gave it
llm_chain.invoke({"input_prompt":"What is my age?"})

7.5.3 ConversationSummary 摘要记忆

第三种策略:不保留原文,而是让 LLM 持续摘要对话内容——信息保留更久且长度可控。先创建一个摘要提示模板:

python
# Create a summary prompt template
summary_prompt_template = """<s><|user|>Summarize the conversations and update with the new lines.

Current summary:
{summary}

new lines of conversation:
{new_lines}

New summary:<|end|>
<|assistant|>"""
summary_prompt = PromptTemplate(
    input_variables=["new_lines", "summary"],
    template=summary_prompt_template
)

引入 ConversationSummaryMemory 并接入链中:

python
from langchain.memory import ConversationSummaryMemory

# Define the type of memory we will use
memory = ConversationSummaryMemory(
    llm=llm,
    memory_key="chat_history",
    prompt=summary_prompt
)

# Chain the LLM, prompt, and memory together
llm_chain = LLMChain(
    prompt=prompt,
    llm=llm,
    memory=memory
)
python
# Generate a conversation and ask for the name
llm_chain.invoke({"input_prompt": "Hi! My name is Maarten. What is 1 + 1?"})
llm_chain.invoke({"input_prompt": "What is my name?"})

即使早期对话已被压缩成摘要,关键信息依然可查:

python
# Check whether it has summarized everything thus far
llm_chain.invoke({"input_prompt": "What was the first question I asked?"})

直接查看当前摘要的内容:

python
# Check what the summary is thus far
memory.load_memory_variables({})

7.6 Agents 智能体

智能体(Agent)是本章的高潮:LLM 不再直接回答问题,而是推理出该调用哪个工具、观察结果、循环往复直到得出最终答案。这就是 ReAct 范式(Reasoning + Acting)。

由于智能体需要较强的指令遵循能力,这里换用 OpenAI 的模型:

python
import os
from langchain_openai import ChatOpenAI

# Load OpenAI's LLMs with LangChain
os.environ["OPENAI_API_KEY"] = "MY_KEY"
openai_llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)

编写 ReAct 提示模板——它规定了 Thought → Action → Action Input → Observation 的思考循环格式:

python
# Create the ReAct template
react_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}"""

prompt = PromptTemplate(
    template=react_template,
    input_variables=["tools", "tool_names", "input", "agent_scratchpad"]
)

准备工具集:DuckDuckGo 网络搜索 + 数学计算:

python
from langchain.agents import load_tools, Tool
from langchain.tools import DuckDuckGoSearchResults

# You can create the tool to pass to an agent
search = DuckDuckGoSearchResults()
search_tool = Tool(
    name="duckduck",
    description="A web search engine. Use this to as a search engine for general queries.",
    func=search.run,
)

# Prepare tools
tools = load_tools(["llm-math"], llm=openai_llm)
tools.append(search_tool)

构造 ReAct 智能体与执行器:

python
from langchain.agents import AgentExecutor, create_react_agent

# Construct the ReAct agent
agent = create_react_agent(openai_llm, tools, prompt)
agent_executor = AgentExecutor(
    agent=agent, tools=tools, verbose=True, handle_parsing_errors=True
)

问一个必须「搜索 + 计算」才能回答的问题:

python
# What is the Price of a MacBook Pro?
agent_executor.invoke(
    {
        "input": "What is the current price of a MacBook Pro in USD? How much would it cost in EUR if the exchange rate is 0.85 EUR for 1 USD?"
    }
)

7.7 本章小结

  • LangChain 的链(Chain)用 prompt | llm 管道语法把提示模板与 LLM 组合成可复用流水线;
  • 多链编排让任务分解成阶段化流水线:本例中标题链 → 主角链 → 故事链依次传递 output_key
  • LLM 无状态,三种记忆策略各有取舍:ConversationBuffer 全量保存、Window 只留最近 k 轮、Summary 用 LLM 持续压缩摘要;
  • ReAct 智能体通过 Thought → Action → Observation 循环自主决策工具调用,配合 DuckDuckGo 搜索与 llm-math 完成多跳任务;
  • GGUF 格式的 Phi-3 mini 可通过 LlamaCpp 在本地 GPU 上高效运行。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. LangChain 中 `basic_chain = prompt | llm` 这行代码的作用是?

2. 三链故事流水线中,主角描述链(character)的输入变量是?

3. 使用 ConversationBufferWindowMemory(k=2) 后,LLM 对最早一轮对话中的信息表现如何?

4. 关于 ReAct 智能体的执行循环,下列哪项描述正确?

🛠️ 动手实践

  1. 把 7.4 节的三链故事流水线扩展为四链:新增一条「结局链」(接收 summary、title、character、story,只返回一句话结局),并用同一个输入 "a girl that lost her mother" 测试完整效果。
  2. 将 7.5.2 节的 ConversationBufferWindowMemory(k=2) 改为 k=1,重复书中的三轮提问实验,记录哪些信息丢失了,并解释窗口大小对记忆能力的影响。
  3. 参考 7.6 节的 ReAct 模板,为智能体再添加一个自定义 Tool(例如返回当前日期的函数),然后提出一个必须同时使用搜索、数学与新工具才能回答的问题,观察 verbose=True 输出的完整推理轨迹。