第 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(或任何其他云环境)中查看笔记本,需要取消注释并运行以下代码块来安装本章依赖:
# %%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.697.2 Loading an LLM 加载语言模型
首先下载 Phi-3 mini 模型的 GGUF 量化权重:
!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 封装加载模型:
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
)测试一下基本问答:
llm.invoke("Hi! My name is Maarten. What is 1 + 1?")7.3 Chains 链
链(Chain)把提示模板和 LLM 组合成一条可复用的流水线。先创建一个带 input_prompt 变量的提示模板:
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 连接成一条基础链:
basic_chain = prompt | llm# Use the chain
basic_chain.invoke(
{
"input_prompt": "Hi! My name is Maarten. What is 1 + 1?",
}
)7.4 Multiple Chains 多条链协作
单个链的能力有限,真正的威力在于把多条链串联起来,让一条链的输出成为另一条链的输入。下面用一个「故事生成」例子演示三步流水线:起标题 → 描述主角 → 写故事。
第一步,创建负责生成故事标题的链:
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")title.invoke({"summary": "a girl that lost her mother"})第二步,创建基于摘要与标题描述主角的链:
# 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")第三步,创建综合前三者输出完整故事的链:
# 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")把三个组件串成完整流水线:
# Combine all three components to create the full chain
llm_chain = title | character | storyllm_chain.invoke("a girl that lost her mother")7.5 Memory 记忆
LLM 天生是「无状态」的——每次调用都是独立的。验证一下:先告诉它名字,
# Let's give the LLM our name
basic_chain.invoke({"input_prompt": "Hi! My name is Maarten. What is 1 + 1?"})再让它复述名字,它会失败:
# 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 变量:
# 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 并把它接入链中:
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
)# Generate a conversation and ask a basic question
llm_chain.invoke({"input_prompt": "Hi! My name is Maarten. What is 1 + 1?"})现在 LLM 能记住我们说过的名字了:
# Does the LLM remember the name we gave it?
llm_chain.invoke({"input_prompt": "What is my name?"})7.5.2 ConversationBufferMemoryWindow 窗口缓冲记忆
缓冲记忆的问题是对话越长提示越大。窗口记忆只保留最近 k 轮对话:
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
)进行两轮对话填充它的记忆窗口:
# 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?"})它还记得名字吗?
# Check whether it knows the name we gave it
llm_chain.invoke({"input_prompt":"What is my name?"})那年龄呢?(第一轮已被挤出窗口)
# Check whether it knows the age we gave it
llm_chain.invoke({"input_prompt":"What is my age?"})7.5.3 ConversationSummary 摘要记忆
第三种策略:不保留原文,而是让 LLM 持续摘要对话内容——信息保留更久且长度可控。先创建一个摘要提示模板:
# 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 并接入链中:
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
)# 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?"})即使早期对话已被压缩成摘要,关键信息依然可查:
# Check whether it has summarized everything thus far
llm_chain.invoke({"input_prompt": "What was the first question I asked?"})直接查看当前摘要的内容:
# Check what the summary is thus far
memory.load_memory_variables({})7.6 Agents 智能体
智能体(Agent)是本章的高潮:LLM 不再直接回答问题,而是推理出该调用哪个工具、观察结果、循环往复直到得出最终答案。这就是 ReAct 范式(Reasoning + Acting)。
由于智能体需要较强的指令遵循能力,这里换用 OpenAI 的模型:
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 的思考循环格式:
# 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 网络搜索 + 数学计算:
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 智能体与执行器:
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
)问一个必须「搜索 + 计算」才能回答的问题:
# 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 智能体的执行循环,下列哪项描述正确?
🛠️ 动手实践
- 把 7.4 节的三链故事流水线扩展为四链:新增一条「结局链」(接收 summary、title、character、story,只返回一句话结局),并用同一个输入
"a girl that lost her mother"测试完整效果。 - 将 7.5.2 节的
ConversationBufferWindowMemory(k=2)改为k=1,重复书中的三轮提问实验,记录哪些信息丢失了,并解释窗口大小对记忆能力的影响。 - 参考 7.6 节的 ReAct 模板,为智能体再添加一个自定义
Tool(例如返回当前日期的函数),然后提出一个必须同时使用搜索、数学与新工具才能回答的问题,观察verbose=True输出的完整推理轨迹。