AI Agents学习笔记(2)

21 阅读1小时+

第四章 智能体经典范式构建

在上一章中,我们深入探讨了作为现代智能体“大脑”的大语言模型。我们了解了其内部的Transformer架构、与之交互的方法,以及它的能力边界。现在,是时候将这些理论知识转化为实践,亲手构建智能体了。

一个现代的智能体,其核心能力在于能将大语言模型的推理能力与外部世界联通。它能够自主地理解用户意图、拆解复杂任务,并通过调用代码解释器、搜索引擎、API等一系列“工具”,来获取信息、执行操作,最终达成目标。 然而,智能体并非万能,它同样面临着来自大模型本身的“幻觉”问题、在复杂任务中可能陷入推理循环、以及对工具的错误使用等挑战,这些也构成了智能体的能力边界。

为了更好地组织智能体的“思考”与“行动”过程,业界涌现出了多种经典的架构范式。在本章中,我们将聚焦于其中最具代表性的三种,并一步步从零实现它们:

  • ReAct (Reasoning and Acting):  一种将“思考”和“行动”紧密结合的范式,让智能体边想边做,动态调整。
  • Plan-and-Solve:  一种“三思而后行”的范式,智能体首先生成一个完整的行动计划,然后严格执行。
  • Reflection:  一种赋予智能体“反思”能力的范式,通过自我批判和修正来优化结果。

了解了这些之后,你可能会问,市面上已有LangChain、LlamaIndex等众多优秀框架,为何还要“重复造轮子”?答案在于,尽管成熟的框架在工程效率上优势显著,但直接使用高度抽象的工具,并不利于我们了解背后的设计机制是怎么运行的,或者是有何好处。其次,这个过程会暴露出项目的工程挑战。框架为我们处理了许多问题,例如模型输出格式的解析、工具调用失败的重试、防止智能体陷入死循环等。亲手处理这些问题,是培养系统设计能力的最直接方式。最后,也是最重要的一点,掌握了设计原理,你才能真正地从一个框架的“使用者”转变为一个智能体应用的“创造者”。当标准组件无法满足你的复杂需求时,你将拥有深度定制乃至从零构建一个全新智能体的能力。

4.1 环境准备与基础工具定义

在开始构建之前,我们需要先搭建好开发环境并定义一些基础组件。这能帮助我们在后续实现不同范式时,避免重复劳动,更专注于核心逻辑。

4.1.1 安装依赖库

本书的实战部分将主要使用 Python 语言,建议使用 Python 3.10 或更高版本。首先,请确保你已经安装了 openai 库用于与大语言模型交互,以及 python-dotenv 库用于安全地管理我们的 API 密钥。

在你的终端中运行以下命令:

pip install openai python-dotenvCopy to clipboardErrorCopied

4.1.2 配置 API 密钥

为了让我们的代码更通用,我们将模型服务的相关信息(模型ID、API密钥、服务地址)统一配置在环境变量中。

  1. 在你的项目根目录下,创建一个名为 .env 的文件。
  2. 在该文件中,添加以下内容。你可以根据自己的需要,将其指向 OpenAI 官方服务,或任何兼容 OpenAI 接口的本地/第三方服务。
  3. 如果实在不知道如何获取,可以参考Datawhale另一本教程的1.2 API设置
# .env file
LLM_API_KEY="YOUR-API-KEY"
LLM_MODEL_ID="YOUR-MODEL"
LLM_BASE_URL="YOUR-URL"Copy to clipboardErrorCopied

我们的代码将从此文件自动加载这些配置。

4.1.3 封装基础 LLM 调用函数

为了让代码结构更清晰、更易于复用,我们来定义一个专属的LLM客户端类。这个类将封装所有与模型服务交互的细节,让我们的主逻辑可以更专注于智能体的构建。

import os
from openai import OpenAI
from dotenv import load_dotenv
from typing import List, Dict

# 加载 .env 文件中的环境变量
load_dotenv()

class HelloAgentsLLM:
    """
    为本书 "Hello Agents" 定制的LLM客户端。
    它用于调用任何兼容OpenAI接口的服务,并默认使用流式响应。
    """
    def __init__(self, model: str = None, apiKey: str = None, baseUrl: str = None, timeout: int = None):
        """
        初始化客户端。优先使用传入参数,如果未提供,则从环境变量加载。
        """
        self.model = model or os.getenv("LLM_MODEL_ID")
        apiKey = apiKey or os.getenv("LLM_API_KEY")
        baseUrl = baseUrl or os.getenv("LLM_BASE_URL")
        timeout = timeout or int(os.getenv("LLM_TIMEOUT", 60))
        
        if not all([self.model, apiKey, baseUrl]):
            raise ValueError("模型ID、API密钥和服务地址必须被提供或在.env文件中定义。")

        self.client = OpenAI(api_key=apiKey, base_url=baseUrl, timeout=timeout)

    def think(self, messages: List[Dict[str, str]], temperature: float = 0) -> str:
        """
        调用大语言模型进行思考,并返回其响应。
        """
        print(f"🧠 正在调用 {self.model} 模型...")
        try:
            response = self.client.chat.completions.create(
                model=self.model,
                messages=messages,
                temperature=temperature,
                stream=True,
            )
            
            # 处理流式响应
            print("✅ 大语言模型响应成功:")
            collected_content = []
            for chunk in response:
                content = chunk.choices[0].delta.content or ""
                print(content, end="", flush=True)
                collected_content.append(content)
            print()  # 在流式输出结束后换行
            return "".join(collected_content)

        except Exception as e:
            print(f"❌ 调用LLM API时发生错误: {e}")
            return None

# --- 客户端使用示例 ---
if __name__ == '__main__':
    try:
        llmClient = HelloAgentsLLM()
        
        exampleMessages = [
            {"role": "system", "content": "You are a helpful assistant that writes Python code."},
            {"role": "user", "content": "写一个快速排序算法"}
        ]
        
        print("--- 调用LLM ---")
        responseText = llmClient.think(exampleMessages)
        if responseText:
            print("\n\n--- 完整模型响应 ---")
            print(responseText)

    except ValueError as e:
        print(e)


>>>
--- 调用LLM ---
🧠 正在调用 xxxxxx 模型...
✅ 大语言模型响应成功:
快速排序是一种非常高效的排序算法...Copy to clipboardErrorCopied

4.2 ReAct

在准备好LLM客户端后,我们将构建第一个,也是最经典的一个智能体范式ReAct (Reason + Act) 。ReAct由Shunyu Yao于2022年提出[1],其核心思想是模仿人类解决问题的方式,将推理 (Reasoning)  与行动 (Acting)  显式地结合起来,形成一个“思考-行动-观察”的循环。

4.2.1 ReAct 的工作流程

在ReAct诞生之前,主流的方法可以分为两类:一类是“纯思考”型,如思维链 (Chain-of-Thought) ,它能引导模型进行复杂的逻辑推理,但无法与外部世界交互,容易产生事实幻觉;另一类是“纯行动”型,模型直接输出要执行的动作,但缺乏规划和纠错能力。

ReAct的巧妙之处在于,它认识到思考与行动是相辅相成的。思考指导行动,而行动的结果又反过来修正思考。为此,ReAct范式通过一种特殊的提示工程来引导模型,使其每一步的输出都遵循一个固定的轨迹:

  • Thought (思考):  这是智能体的“内心独白”。它会分析当前情况、分解任务、制定下一步计划,或者反思上一步的结果。
  • Action (行动):  这是智能体决定采取的具体动作,通常是调用一个外部工具,例如 Search['华为最新款手机']
  • Observation (观察):  这是执行Action后从外部工具返回的结果,例如搜索结果的摘要或API的返回值。

智能体将不断重复这个 Thought -> Action -> Observation 的循环,将新的观察结果追加到历史记录中,形成一个不断增长的上下文,直到它在Thought中认为已经找到了最终答案,然后输出结果。这个过程形成了一个强大的协同效应:推理使得行动更具目的性,而行动则为推理提供了事实依据。

我们可以将这个过程形式化地表达出来,如图4.1所示。具体来说,在每个时间步 tt,智能体的策略(即大语言模型 ππ)会根据初始问题 qq 和之前所有步骤的“行动-观察”历史轨迹 ((a1,o1),…,(at−1,ot−1))((a1​,o1​),…,(at−1​,ot−1​)),来生成当前的思考 thttht​ 和行动 atat​:

(tht,at)=π(q,(a1,o1),…,(at−1,ot−1))(tht​,at​)=π(q,(a1​,o1​),…,(at−1​,ot−1​))

随后,环境中的工具 TT 会执行行动 atat​,并返回一个新的观察结果 otot​:

ot=T(at)ot​=T(at​)

这个循环不断进行,将新的 (at,ot)(at​,ot​) 对追加到历史中,直到模型在思考 thttht​ 中判断任务已完成。

ReAct范式中的“思考-行动-观察”协同循环

图 4.1 ReAct 范式中的“思考-行动-观察”协同循环

这种机制特别适用于以下场景:

  • 需要外部知识的任务:如查询实时信息(天气、新闻、股价)、搜索专业领域的知识等。
  • 需要精确计算的任务:将数学问题交给计算器工具,避免LLM的计算错误。
  • 需要与API交互的任务:如操作数据库、调用某个服务的API来完成特定功能。

因此我们将构建一个具备使用外部工具能力的ReAct智能体,来回答一个大语言模型仅凭自身知识库无法直接回答的问题。例如:“华为最新的手机是哪一款?它的主要卖点是什么?” 这个问题需要智能体理解自己需要上网搜索,调用工具搜索结果并总结答案。

4.2.2 工具的定义与实现

如果说大语言模型是智能体的大脑,那么工具 (Tools)  就是其与外部世界交互的“手和脚”。为了让ReAct范式能够真正解决我们设定的问题,智能体需要具备调用外部工具的能力。

针对本节设定的目标——回答关于“华为最新手机”的问题,我们需要为智能体提供一个网页搜索工具。在这里我们选用 SerpApi,它通过API提供结构化的Google搜索结果,能直接返回“答案摘要框”或精确的知识图谱信息,

首先,需要安装该库:

pip install google-search-resultsCopy to clipboardErrorCopied

同时,你需要前往 SerpApi官网 注册一个免费账户,获取你的API密钥,并将其添加到我们项目根目录下的 .env 文件中:

# .env file
# ... (保留之前的LLM配置)
SERPAPI_API_KEY="YOUR_SERPAPI_API_KEY"Copy to clipboardErrorCopied

接下来,我们通过代码来定义和管理这个工具。我们将分步进行:首先实现工具的核心功能,然后构建一个通用的工具管理器。

(1)实现搜索工具的核心逻辑

一个良好定义的工具应包含以下三个核心要素:

  1. 名称 (Name) : 一个简洁、唯一的标识符,供智能体在 Action 中调用,例如 Search
  2. 描述 (Description) : 一段清晰的自然语言描述,说明这个工具的用途。这是整个机制中最关键的部分,因为大语言模型会依赖这段描述来判断何时使用哪个工具。
  3. 执行逻辑 (Execution Logic) : 真正执行任务的函数或方法。

我们的第一个工具是 search 函数,它的作用是接收一个查询字符串,然后返回搜索结果。

from serpapi import SerpApiClient

def search(query: str) -> str:
    """
    一个基于SerpApi的实战网页搜索引擎工具。
    它会智能地解析搜索结果,优先返回直接答案或知识图谱信息。
    """
    print(f"🔍 正在执行 [SerpApi] 网页搜索: {query}")
    try:
        api_key = os.getenv("SERPAPI_API_KEY")
        if not api_key:
            return "错误:SERPAPI_API_KEY 未在 .env 文件中配置。"

        params = {
            "engine": "google",
            "q": query,
            "api_key": api_key,
            "gl": "cn",  # 国家代码
            "hl": "zh-cn", # 语言代码
        }
        
        client = SerpApiClient(params)
        results = client.get_dict()
        
        # 智能解析:优先寻找最直接的答案
        if "answer_box_list" in results:
            return "\n".join(results["answer_box_list"])
        if "answer_box" in results and "answer" in results["answer_box"]:
            return results["answer_box"]["answer"]
        if "knowledge_graph" in results and "description" in results["knowledge_graph"]:
            return results["knowledge_graph"]["description"]
        if "organic_results" in results and results["organic_results"]:
            # 如果没有直接答案,则返回前三个有机结果的摘要
            snippets = [
                f"[{i+1}] {res.get('title', '')}\n{res.get('snippet', '')}"
                for i, res in enumerate(results["organic_results"][:3])
            ]
            return "\n\n".join(snippets)
        
        return f"对不起,没有找到关于 '{query}' 的信息。"

    except Exception as e:
        return f"搜索时发生错误: {e}"Copy to clipboardErrorCopied

在上述代码中,首先会检查是否存在 answer_box(Google的答案摘要框)或 knowledge_graph(知识图谱)等信息,如果存在,就直接返回这些最精确的答案。如果不存在,它才会退而求其次,返回前三个常规搜索结果的摘要。这种“智能解析”能为LLM提供质量更高的信息输入。

(2)构建通用的工具执行器

当智能体需要使用多种工具时(例如,除了搜索,还可能需要计算、查询数据库等),我们需要一个统一的管理器来注册和调度这些工具。为此,我们创建一个 ToolExecutor 类。

from typing import Dict, Any

class ToolExecutor:
    """
    一个工具执行器,负责管理和执行工具。
    """
    def __init__(self):
        self.tools: Dict[str, Dict[str, Any]] = {}

    def registerTool(self, name: str, description: str, func: callable):
        """
        向工具箱中注册一个新工具。
        """
        if name in self.tools:
            print(f"警告:工具 '{name}' 已存在,将被覆盖。")
        self.tools[name] = {"description": description, "func": func}
        print(f"工具 '{name}' 已注册。")

    def getTool(self, name: str) -> callable:
        """
        根据名称获取一个工具的执行函数。
        """
        return self.tools.get(name, {}).get("func")

    def getAvailableTools(self) -> str:
        """
        获取所有可用工具的格式化描述字符串。
        """
        return "\n".join([
            f"- {name}: {info['description']}" 
            for name, info in self.tools.items()
        ])
Copy to clipboardErrorCopied

(3)测试

现在,我们将 search 工具注册到 ToolExecutor 中,并模拟一次调用,以验证整个流程是否正常工作。

# --- 工具初始化与使用示例 ---
if __name__ == '__main__':
    # 1. 初始化工具执行器
    toolExecutor = ToolExecutor()

    # 2. 注册我们的实战搜索工具
    search_description = "一个网页搜索引擎。当你需要回答关于时事、事实以及在你的知识库中找不到的信息时,应使用此工具。"
    toolExecutor.registerTool("Search", search_description, search)
    
    # 3. 打印可用的工具
    print("\n--- 可用的工具 ---")
    print(toolExecutor.getAvailableTools())

    # 4. 智能体的Action调用,这次我们问一个实时性的问题
    print("\n--- 执行 Action: Search['英伟达最新的GPU型号是什么'] ---")
    tool_name = "Search"
    tool_input = "英伟达最新的GPU型号是什么"

    tool_function = toolExecutor.getTool(tool_name)
    if tool_function:
        observation = tool_function(tool_input)
        print("--- 观察 (Observation) ---")
        print(observation)
    else:
        print(f"错误:未找到名为 '{tool_name}' 的工具。")
        
>>>
工具 'Search' 已注册。

--- 可用的工具 ---
- Search: 一个网页搜索引擎。当你需要回答关于时事、事实以及在你的知识库中找不到的信息时,应使用此工具。

--- 执行 Action: Search['英伟达最新的GPU型号是什么'] ---
🔍 正在执行 [SerpApi] 网页搜索: 英伟达最新的GPU型号是什么
--- 观察 (Observation) ---
[1] GeForce RTX 50 系列显卡
GeForce RTX™ 50 系列GPU 搭载NVIDIA Blackwell 架构,为游戏玩家和创作者带来全新玩法。RTX 50 系列具备强大的AI 算力,带来升级体验和更逼真的画面。

[2] 比较GeForce 系列最新一代显卡和前代显卡
比较最新一代RTX 30 系列显卡和前代的RTX 20 系列、GTX 10 和900 系列显卡。查看规格、功能、技术支持等内容。

[3] GeForce 显卡| NVIDIA
DRIVE AGX. 强大的车载计算能力,适用于AI 驱动的智能汽车系统 · Clara AGX. 适用于创新型医疗设备和成像的AI 计算. 游戏和创作. GeForce. 探索显卡、游戏解决方案、AI ...Copy to clipboardErrorCopied

至此,我们已经为智能体配备了连接真实世界互联网的Search工具,为后续的ReAct循环提供了坚实的基础。

4.2.3 ReAct 智能体的编码实现

现在,我们将所有独立的组件,LLM客户端和工具执行器组装起来,构建一个完整的 ReAct 智能体。我们将通过一个 ReActAgent 类来封装其核心逻辑。为了便于理解,我们将这个类的实现过程拆分为以下几个关键部分进行讲解。

(1)系统提示词设计

提示词是整个 ReAct 机制的基石,它为大语言模型提供了行动的操作指令。我们需要精心设计一个模板,它将动态地插入可用工具、用户问题以及中间步骤的交互历史。

# ReAct 提示词模板
REACT_PROMPT_TEMPLATE = """
请注意,你是一个有能力调用外部工具的智能助手。

可用工具如下:
{tools}

请严格按照以下格式进行回应:

Thought: 你的思考过程,用于分析问题、拆解任务和规划下一步行动。
Action: 你决定采取的行动,必须是以下格式之一:
- `{{tool_name}}[{{tool_input}}]`:调用一个可用工具。
- `Finish[最终答案]`:当你认为已经获得最终答案时。
- 当你收集到足够的信息,能够回答用户的最终问题时,你必须在Action:字段后使用 finish(answer="...") 来输出最终答案。

现在,请开始解决以下问题:
Question: {question}
History: {history}
"""Copy to clipboardErrorCopied

这个模板定义了智能体与LLM之间交互的规范:

  • 角色定义: “你是一个有能力调用外部工具的智能助手”,设定了LLM的角色。
  • 工具清单 ({tools}) : 告知LLM它有哪些可用的“手脚”。
  • 格式规约 (Thought/Action) : 这是最重要的部分,它强制LLM的输出具有结构性,使我们能通过代码精确解析其意图。
  • 动态上下文 ({question}/{history}) : 将用户的原始问题和不断累积的交互历史注入,让LLM基于完整的上下文进行决策。

(2)核心循环的实现

ReActAgent 的核心是一个循环,它不断地“格式化提示词 -> 调用LLM -> 执行动作 -> 整合结果”,直到任务完成或达到最大步数限制。

class ReActAgent:
    def __init__(self, llm_client: HelloAgentsLLM, tool_executor: ToolExecutor, max_steps: int = 5):
        self.llm_client = llm_client
        self.tool_executor = tool_executor
        self.max_steps = max_steps
        self.history = []

    def run(self, question: str):
        """
        运行ReAct智能体来回答一个问题。
        """
        self.history = [] # 每次运行时重置历史记录
        current_step = 0

        while current_step < self.max_steps:
            current_step += 1
            print(f"--- 第 {current_step} 步 ---")

            # 1. 格式化提示词
            tools_desc = self.tool_executor.getAvailableTools()
            history_str = "\n".join(self.history)
            prompt = REACT_PROMPT_TEMPLATE.format(
                tools=tools_desc,
                question=question,
                history=history_str
            )

            # 2. 调用LLM进行思考
            messages = [{"role": "user", "content": prompt}]
            response_text = self.llm_client.think(messages=messages)
            
            if not response_text:
                print("错误:LLM未能返回有效响应。")
                break

            # ... (后续的解析、执行、整合步骤)
Copy to clipboardErrorCopied

run 方法是智能体的入口。它的 while 循环构成了 ReAct 范式的主体,max_steps 参数则是一个重要的安全阀,防止智能体陷入无限循环而耗尽资源。

(3)输出解析器的实现

LLM 返回的是纯文本,我们需要从中精确地提取出ThoughtAction。这是通过几个辅助解析函数完成的,它们通常使用正则表达式来实现。

# (这些方法是 ReActAgent 类的一部分)
    def _parse_output(self, text: str):
        """解析LLM的输出,提取Thought和Action。"""
        thought_match = re.search(r"Thought: (.*)", text)
        action_match = re.search(r"Action: (.*)", text)
        thought = thought_match.group(1).strip() if thought_match else None
        action = action_match.group(1).strip() if action_match else None
        return thought, action

    def _parse_action(self, action_text: str):
        """解析Action字符串,提取工具名称和输入。"""
        match = re.match(r"(\w+)[(.*)]", action_text)
        if match:
            return match.group(1), match.group(2)
        return None, NoneCopy to clipboardErrorCopied
  • _parse_output: 负责从LLM的完整响应中分离出ThoughtAction两个主要部分。
  • _parse_action: 负责进一步解析Action字符串,例如从 Search[华为最新手机] 中提取出工具名 Search 和工具输入 华为最新手机

(4) 工具调用与执行

# (这段逻辑在 run 方法的 while 循环内)
            # 3. 解析LLM的输出
            thought, action = self._parse_output(response_text)
            
            if thought:
                print(f"思考: {thought}")

            if not action:
                print("警告:未能解析出有效的Action,流程终止。")
                break

            # 4. 执行Action
            if action.startswith("Finish"):
                # 如果是Finish指令,提取最终答案并结束
                final_answer = re.match(r"Finish[(.*)]", action).group(1)
                print(f"🎉 最终答案: {final_answer}")
                return final_answer
            
            tool_name, tool_input = self._parse_action(action)
            if not tool_name or not tool_input:
                # ... 处理无效Action格式 ...
                continue

            print(f"🎬 行动: {tool_name}[{tool_input}]")
            
            tool_function = self.tool_executor.getTool(tool_name)
            if not tool_function:
                observation = f"错误:未找到名为 '{tool_name}' 的工具。"
            else:
                observation = tool_function(tool_input) # 调用真实工具
Copy to clipboardErrorCopied

这段代码是Action的执行中心。它首先检查是否为Finish指令,如果是,则流程结束。否则,它会通过tool_executor获取对应的工具函数并执行,得到observation

(5)观测结果的整合

最后一步,也是形成闭环的关键,是将Action本身和工具执行后的Observation添加回历史记录中,为下一轮循环提供新的上下文。

# (这段逻辑紧随工具调用之后,在 while 循环的末尾)
            print(f"👀 观察: {observation}")
            
            # 将本轮的Action和Observation添加到历史记录中
            self.history.append(f"Action: {action}")
            self.history.append(f"Observation: {observation}")

        # 循环结束
        print("已达到最大步数,流程终止。")
        return NoneCopy to clipboardErrorCopied

通过将Observation追加到self.history,智能体在下一轮生成提示词时,就能“看到”上一步行动的结果,并据此进行新一轮的思考和规划。

(6)运行实例与分析

将以上所有部分组合起来,我们就得到了完整的 ReActAgent 类。完整的代码运行实例可以在本书配套的代码仓库 code 文件夹中找到。

下面是一次真实的运行记录:

工具 'Search' 已注册。

--- 第 1 步 ---
🧠 正在调用 xxxxxx 模型...
✅ 大语言模型响应成功:
Thought: 要回答这个问题,我需要查找华为最新发布的手机型号及其主要特点。这些信息可能在我的现有知识库之外,因此需要使用搜索引擎来获取最新数据。
Action: Search[华为最新手机型号及主要卖点]
🤔 思考: 要回答这个问题,我需要查找华为最新发布的手机型号及其主要特点。这些信息可能在我的现有知识库之外,因此需要使用搜索引擎来获取最新数据。
🎬 行动: Search[华为最新手机型号及主要卖点]
🔍 正在执行 [SerpApi] 网页搜索: 华为最新手机型号及主要卖点
👀 观察: [1] 华为手机- 华为官网
智能手机 ; Mate 系列. 非凡旗舰 · HUAWEI Mate XTs. 非凡大师 ; Pura 系列. 先锋影像 · HUAWEI Pura 80 Pro+ ; Pocket 系列. 美学新篇. HUAWEI Pocket 2 ; nova 系列. 专业人像.

[2] 2025年华为手机哪一款性价比高?华为手机推荐与市场分析 ...
现在华为手机最大的卖点只剩下鸿蒙HarmonyOS系统,以及饱受争议的品牌信仰。 这里推荐目前值得入手的几款华为系列手机,根据不同预算自行选择:. 华为目前最受欢迎,也是搭载 ...

[3] 2025年华为新款手机哪个性价比高?10款华为新款手机推荐
选华为主要还是要推荐高端手机,Mate 70和Pura 70系列是最新发布的旗舰机型。 HUAWEI Mate 70. 优点是,拍照配置依旧顶级,全焦段覆盖,适合专业摄影,做工出色,户外抗摔 ...

--- 第 2 步 ---
🧠 正在调用 xxxxxx 模型...
✅ 大语言模型响应成功:
Thought: 根据搜索结果,华为最新发布的旗舰机型包括Mate 70和Pura 80 Pro+。为了确定最新型号及其主要卖点,我将重点放在这些信息上。从提供的链接来看,Mate 70系列和Pura 80 Pro+都是近期发布的产品,但具体哪一个是“最新”还需要进一步确认。同时,我可以从这些信息中提取出它们的主要
卖点。
Action: Finish[根据最新信息,华为的最新手机可能是HUAWEI Pura 80 Pro+或HUAWEI Mate 70。其中,HUAWEI Mate 70的主要卖点包括顶级的拍照配置,全焦段覆盖,适合专业摄影,做工出色,并且具有良好的户外抗摔性能。而HUAWEI Pura 80 Pro+则强调了先锋影像技术。]
🤔 思考: 根据搜索结果,华为最新发布的旗舰机型包括Mate 70和Pura 80 Pro+。为了确定最新型号及其主要卖点,我将重点放在这些信息上。从提供的链接来看,Mate 70系列和Pura 80 Pro+都是近期发布的产品,但具体哪一个是“最新”还需要进一步确认。同时,我可以从这些信息中提取出它们的主要 
卖点。
🎉 最终答案: 根据最新信息,华为的最新手机可能是HUAWEI Pura 80 Pro+或HUAWEI Mate 70。其中,HUAWEI Mate 70的主要卖点包括顶级的拍照配置,全焦段覆盖,适合专业摄影,做工出色,并且具有良好的户外抗摔性能。而HUAWEI Pura 80 Pro+则强调了先锋影像技术。Copy to clipboardErrorCopied

从上面的输出可以看到,智能体清晰地展示了它的思考链条:它首先意识到自己的知识不足,需要使用搜索工具;然后,它根据搜索结果进行推理和总结,并在两步之内得出了最终答案。

值得注意的是,由于模型的知识和互联网的信息是不断更新的,你运行的结果可能与此不完全相同。截止本节内容编写的2025年9月8日,搜索结果中提到的HUAWEI Mate 70与HUAWEI Pura 80 Pro+确实是华为当时最新的旗舰系列手机。这充分展示了ReAct范式在处理时效性问题上的强大能力。

4.2.4 ReAct 的特点、局限性与调试技巧

通过亲手实现一个 ReAct 智能体,我们不仅掌握了其工作流程,也应该对其内在机制有了更深刻的认识。任何技术范式都有其闪光点和待改进之处,本节将对 ReAct 进行总结。

(1)ReAct 的主要特点

  1. 高可解释性:ReAct 最大的优点之一就是透明。通过 Thought 链,我们可以清晰地看到智能体每一步的“心路历程”——它为什么会选择这个工具,下一步又打算做什么。这对于理解、信任和调试智能体的行为至关重要。
  2. 动态规划与纠错能力:与一次性生成完整计划的范式不同,ReAct 是“走一步,看一步”。它根据每一步从外部世界获得的 Observation 来动态调整后续的 Thought 和 Action。如果上一步的搜索结果不理想,它可以在下一步中修正搜索词,重新尝试。
  3. 工具协同能力:ReAct 范式天然地将大语言模型的推理能力与外部工具的执行能力结合起来。LLM 负责运筹帷幄(规划和推理),工具负责解决具体问题(搜索、计算),二者协同工作,突破了单一 LLM 在知识时效性、计算准确性等方面的固有局限。

(2)ReAct 的固有局限性

  1. 对LLM自身能力的强依赖:ReAct 流程的成功与否,高度依赖于底层 LLM 的综合能力。如果 LLM 的逻辑推理能力、指令遵循能力或格式化输出能力不足,就很容易在 Thought 环节产生错误的规划,或者在 Action 环节生成不符合格式的指令,导致整个流程中断。
  2. 执行效率问题:由于其循序渐进的特性,完成一个任务通常需要多次调用 LLM。每一次调用都伴随着网络延迟和计算成本。对于需要很多步骤的复杂任务,这种串行的“思考-行动”循环可能会导致较高的总耗时和费用。
  3. 提示词的脆弱性:整个机制的稳定运行建立在一个精心设计的提示词模板之上。模板中的任何微小变动,甚至是用词的差异,都可能影响 LLM 的行为。此外,并非所有模型都能持续稳定地遵循预设的格式,这增加了在实际应用中的不确定性。
  4. 可能陷入局部最优:步进式的决策模式意味着智能体缺乏一个全局的、长远的规划。它可能会因为眼前的 Observation 而选择一个看似正确但长远来看并非最优的路径,甚至在某些情况下陷入“原地打转”的循环中。

(3)调试技巧

当你构建的 ReAct 智能体行为不符合预期时,可以从以下几个方面入手进行调试:

  • 检查完整的提示词:在每次调用 LLM 之前,将最终格式化好的、包含所有历史记录的完整提示词打印出来。这是追溯 LLM 决策源头的最直接方式。
  • 分析原始输出:当输出解析失败时(例如,正则表达式没有匹配到 Action),务必将 LLM 返回的原始、未经处理的文本打印出来。这能帮助你判断是 LLM 没有遵循格式,还是你的解析逻辑有误。
  • 验证工具的输入与输出:检查智能体生成的 tool_input 是否是工具函数所期望的格式,同时也要确保工具返回的 observation 格式是智能体可以理解和处理的。
  • 调整提示词中的示例 (Few-shot Prompting) :如果模型频繁出错,可以在提示词中加入一两个完整的“Thought-Action-Observation”成功案例,通过示例来引导模型更好地遵循你的指令。
  • 尝试不同的模型或参数:更换一个能力更强的模型,或者调整 temperature 参数(通常设为0以保证输出的确定性),有时能直接解决问题。

4.3 Plan-and-Solve

在我们掌握了 ReAct 这种反应式的、步进决策的智能体范式后,接下来将探讨一种风格迥异但同样强大的方法,Plan-and-Solve。顾名思义,这种范式将任务处理明确地分为两个阶段:先规划 (Plan),后执行 (Solve)

如果说 ReAct 像一个经验丰富的侦探,根据现场的蛛丝马迹(Observation)一步步推理,随时调整自己的调查方向;那么 Plan-and-Solve 则更像一位建筑师,在动工之前必须先绘制出完整的蓝图(Plan),然后严格按照蓝图来施工(Solve)。事实上我们现在用的很多大模型工具的Agent模式都融入了这种设计模式。

4.3.1 Plan-and-Solve 的工作原理

Plan-and-Solve Prompting 由 Lei Wang 在2023年提出[2]。其核心动机是为了解决思维链在处理多步骤、复杂问题时容易“偏离轨道”的问题。

与 ReAct 将思考和行动融合在每一步不同,Plan-and-Solve 将整个流程解耦为两个核心阶段,如图4.2所示:

  1. 规划阶段 (Planning Phase) : 首先,智能体会接收用户的完整问题。它的第一个任务不是直接去解决问题或调用工具,而是将问题分解,并制定出一个清晰、分步骤的行动计划。这个计划本身就是一次大语言模型的调用产物。
  2. 执行阶段 (Solving Phase) : 在获得完整的计划后,智能体进入执行阶段。它会严格按照计划中的步骤,逐一执行。每一步的执行都可能是一次独立的 LLM 调用,或者是对上一步结果的加工处理,直到计划中的所有步骤都完成,最终得出答案。

这种“先谋后动”的策略,使得智能体在处理需要长远规划的复杂任务时,能够保持更高的目标一致性,避免在中间步骤中迷失方向。

我们可以将这个两阶段过程进行形式化表达。首先,规划模型 πplanπplan​ 根据原始问题 qq 生成一个包含 nn 个步骤的计划 P=(p1,p2,…,pn)P=(p1​,p2​,…,pn​):

P=πplan(q)P=πplan​(q)

随后,在执行阶段,执行模型 πsolveπsolve​ 会逐一完成计划中的步骤。对于第 ii 个步骤,其解决方案 sisi​ 的生成会同时依赖于原始问题 qq、完整计划 PP 以及之前所有步骤的执行结果 (s1,…,si−1)(s1​,…,si−1​):

si=πsolve(q,P,(s1,…,si−1))si​=πsolve​(q,P,(s1​,…,si−1​))

最终的答案就是最后一个步骤的执行结果 snsn​。

Plan-and-Solve范式的两阶段工作流

图 4.2 Plan-and-Solve 范式的两阶段工作流

Plan-and-Solve 尤其适用于那些结构性强、可以被清晰分解的复杂任务,例如:

  • 多步数学应用题:需要先列出计算步骤,再逐一求解。
  • 需要整合多个信息源的报告撰写:需要先规划好报告结构(引言、数据来源A、数据来源B、总结),再逐一填充内容。
  • 代码生成任务:需要先构思好函数、类和模块的结构,再逐一实现。

4.3.2 规划阶段

为了凸显 Plan-and-Solve 范式在结构化推理任务上的优势,我们将不使用工具的方式,而是通过提示词的设计,完成一个推理任务。

这类任务的特点是,答案无法通过单次查询或计算得出,必须先将问题分解为一系列逻辑连贯的子步骤,然后按顺序求解。这恰好能发挥 Plan-and-Solve “先规划,后执行”的核心能力。

我们的目标问题是: “一个水果店周一卖出了15个苹果。周二卖出的苹果数量是周一的两倍。周三卖出的数量比周二少了5个。请问这三天总共卖出了多少个苹果?”

这个问题对于大语言模型来说并不算特别困难,但它包含了一个清晰的逻辑链条可供参考。在某些实际的逻辑难题上,如果大模型不能高质量的推理出准确的答案,可以参考这个设计模式来设计自己的Agent完成任务。智能体需要:

  1. 规划阶段:首先,将问题分解为三个独立的计算步骤(计算周二销量、计算周三销量、计算总销量)。
  2. 执行阶段:然后,严格按照计划,一步步执行计算,并将每一步的结果作为下一步的输入,最终得出总和。

规划阶段的目标是让大语言模型接收原始问题,并输出一个清晰、分步骤的行动计划。这个计划必须是结构化的,以便我们的代码可以轻松解析并逐一执行。因此,我们设计的提示词需要明确地告诉模型它的角色和任务,并给出一个输出格式的范例。

PLANNER_PROMPT_TEMPLATE = """
你是一个顶级的AI规划专家。你的任务是将用户提出的复杂问题分解成一个由多个简单步骤组成的行动计划。
请确保计划中的每个步骤都是一个独立的、可执行的子任务,并且严格按照逻辑顺序排列。
你的输出必须是一个Python列表,其中每个元素都是一个描述子任务的字符串。

问题: {question}

请严格按照以下格式输出你的计划,```python与```作为前后缀是必要的:
```python
["步骤1", "步骤2", "步骤3", ...]
```
"""Copy to clipboardErrorCopied

这个提示词通过以下几点确保了输出的质量和稳定性:

  • 角色设定: “顶级的AI规划专家”,激发模型的专业能力。
  • 任务描述: 清晰地定义了“分解问题”的目标。
  • 格式约束: 强制要求输出为一个 Python 列表格式的字符串,这极大地简化了后续代码的解析工作,使其比解析自然语言更稳定、更可靠。

接下来,我们将这个提示词逻辑封装成一个 Planner 类,这个类也是我们的规划器。

# 假定 llm_client.py 中的 HelloAgentsLLM 类已经定义好
# from llm_client import HelloAgentsLLM

class Planner:
    def __init__(self, llm_client):
        self.llm_client = llm_client

    def plan(self, question: str) -> list[str]:
        """
        根据用户问题生成一个行动计划。
        """
        prompt = PLANNER_PROMPT_TEMPLATE.format(question=question)
        
        # 为了生成计划,我们构建一个简单的消息列表
        messages = [{"role": "user", "content": prompt}]
        
        print("--- 正在生成计划 ---")
        # 使用流式输出来获取完整的计划
        response_text = self.llm_client.think(messages=messages) or ""
        
        print(f"✅ 计划已生成:\n{response_text}")
        
        # 解析LLM输出的列表字符串
        try:
            # 找到```python和```之间的内容
            plan_str = response_text.split("```python")[1].split("```")[0].strip()
            # 使用ast.literal_eval来安全地执行字符串,将其转换为Python列表
            plan = ast.literal_eval(plan_str)
            return plan if isinstance(plan, list) else []
        except (ValueError, SyntaxError, IndexError) as e:
            print(f"❌ 解析计划时出错: {e}")
            print(f"原始响应: {response_text}")
            return []
        except Exception as e:
            print(f"❌ 解析计划时发生未知错误: {e}")
            return []Copy to clipboardErrorCopied

4.3.3 执行器与状态管理

在规划器 (Planner) 生成了清晰的行动蓝图后,我们就需要一个执行器 (Executor) 来逐一完成计划中的任务。执行器不仅负责调用大语言模型来解决每个子问题,还承担着一个至关重要的角色:状态管理。它必须记录每一步的执行结果,并将其作为上下文提供给后续步骤,确保信息在整个任务链条中顺畅流动

执行器的提示词与规划器不同。它的目标不是分解问题,而是在已有上下文的基础上,专注解决当前这一个步骤。因此,提示词需要包含以下关键信息:

  • 原始问题: 确保模型始终了解最终目标。
  • 完整计划: 让模型了解当前步骤在整个任务中的位置。
  • 历史步骤与结果: 提供至今为止已经完成的工作,作为当前步骤的直接输入。
  • 当前步骤: 明确指示模型现在需要解决哪一个具体任务。
EXECUTOR_PROMPT_TEMPLATE = """
你是一位顶级的AI执行专家。你的任务是严格按照给定的计划,一步步地解决问题。
你将收到原始问题、完整的计划、以及到目前为止已经完成的步骤和结果。
请你专注于解决“当前步骤”,并仅输出该步骤的最终答案,不要输出任何额外的解释或对话。

# 原始问题:
{question}

# 完整计划:
{plan}

# 历史步骤与结果:
{history}

# 当前步骤:
{current_step}

请仅输出针对“当前步骤”的回答:
"""Copy to clipboardErrorCopied

我们将执行逻辑封装到 Executor 类中。这个类将循环遍历计划,调用 LLM,并维护一个历史记录(状态)。

class Executor:
    def __init__(self, llm_client):
        self.llm_client = llm_client

    def execute(self, question: str, plan: list[str]) -> str:
        """
        根据计划,逐步执行并解决问题。
        """
        history = "" # 用于存储历史步骤和结果的字符串
        
        print("\n--- 正在执行计划 ---")
        
        for i, step in enumerate(plan):
            print(f"\n-> 正在执行步骤 {i+1}/{len(plan)}: {step}")
            
            prompt = EXECUTOR_PROMPT_TEMPLATE.format(
                question=question,
                plan=plan,
                history=history if history else "无", # 如果是第一步,则历史为空
                current_step=step
            )
            
            messages = [{"role": "user", "content": prompt}]
            
            response_text = self.llm_client.think(messages=messages) or ""
            
            # 更新历史记录,为下一步做准备
            history += f"步骤 {i+1}: {step}\n结果: {response_text}\n\n"
            
            print(f"✅ 步骤 {i+1} 已完成,结果: {response_text}")

        # 循环结束后,最后一步的响应就是最终答案
        final_answer = response_text
        return final_answerCopy to clipboardErrorCopied

现在已经分别构建了负责“规划”的 Planner 和负责“执行”的 Executor。最后一步是将这两个组件整合到一个统一的智能体 PlanAndSolveAgent 中,并赋予它解决问题的完整能力。我们将创建一个主类 PlanAndSolveAgent,它的职责非常清晰:接收一个 LLM 客户端,初始化内部的规划器和执行器,并提供一个简单的 run 方法来启动整个流程。

class PlanAndSolveAgent:
    def __init__(self, llm_client):
        """
        初始化智能体,同时创建规划器和执行器实例。
        """
        self.llm_client = llm_client
        self.planner = Planner(self.llm_client)
        self.executor = Executor(self.llm_client)

    def run(self, question: str):
        """
        运行智能体的完整流程:先规划,后执行。
        """
        print(f"\n--- 开始处理问题 ---\n问题: {question}")
        
        # 1. 调用规划器生成计划
        plan = self.planner.plan(question)
        
        # 检查计划是否成功生成
        if not plan:
            print("\n--- 任务终止 --- \n无法生成有效的行动计划。")
            return

        # 2. 调用执行器执行计划
        final_answer = self.executor.execute(question, plan)
        
        print(f"\n--- 任务完成 ---\n最终答案: {final_answer}")Copy to clipboardErrorCopied

这个 PlanAndSolveAgent 类的设计体现了“组合优于继承”的原则。它本身不包含复杂的逻辑,而是作为一个协调者 (Orchestrator),清晰地调用其内部组件来完成任务。

4.3.4 运行实例与分析

完整的代码同样参考本书配套的代码仓库 code 文件夹,这里只演示最终结果。

--- 开始处理问题 ---
问题: 一个水果店周一卖出了15个苹果。周二卖出的苹果数量是周一的两倍。周三卖出的数量比周二少了5个。请问这三天总共卖出了多少个苹果?
--- 正在生成计划 ---
🧠 正在调用 xxxx 模型...
✅ 大语言模型响应成功:
```python
["计算周一卖出的苹果数量: 15个", "计算周二卖出的苹果数量: 周一数量 × 2 = 15 × 2 = 30个", "计算周三卖出的苹果数量: 周二数量 - 5 = 30 - 5 = 25个", "计算三天总销量: 周一 + 周二 + 周三 = 15 + 30 + 25 = 70个"]
```
✅ 计划已生成:
```python
["计算周一卖出的苹果数量: 15个", "计算周二卖出的苹果数量: 周一数量 × 2 = 15 × 2 = 30个", "计算周三卖出的苹果数量: 周二数量 - 5 = 30 - 5 = 25个", "计算三天总销量: 周一 + 周二 + 周三 = 15 + 30 + 25 = 70个"]
```

--- 正在执行计划 ---

-> 正在执行步骤 1/4: 计算周一卖出的苹果数量: 15个
🧠 正在调用 xxxx 模型...
✅ 大语言模型响应成功:
15
✅ 步骤 1 已完成,结果: 15

-> 正在执行步骤 2/4: 计算周二卖出的苹果数量: 周一数量 × 2 = 15 × 2 = 30个
🧠 正在调用 xxxx 模型...
✅ 大语言模型响应成功:
30
✅ 步骤 2 已完成,结果: 30

-> 正在执行步骤 3/4: 计算周三卖出的苹果数量: 周二数量 - 5 = 30 - 5 = 25个
🧠 正在调用 xxxx 模型...
✅ 大语言模型响应成功:
25
✅ 步骤 3 已完成,结果: 25

-> 正在执行步骤 4/4: 计算三天总销量: 周一 + 周二 + 周三 = 15 + 30 + 25 = 70个
🧠 正在调用 xxxx 模型...
✅ 大语言模型响应成功:
70
✅ 步骤 4 已完成,结果: 70

--- 任务完成 ---
最终答案: 70Copy to clipboardErrorCopied

从上面的输出日志中,我们可以清晰地看到 Plan-and-Solve 范式的工作流程:

  1. 规划阶段: 智能体首先调用 Planner,成功地将复杂的应用题分解成了一个包含四个逻辑步骤的 Python 列表。这个结构化的计划为后续的执行奠定了基础。
  2. 执行阶段: Executor 严格按照生成的计划,一步一步地向下执行。在每一步中,它都将历史结果作为上下文,确保了信息的正确传递(例如,步骤2正确地使用了步骤1的结果“15个”,步骤3也正确使用了步骤2的结果“30个”)。
  3. 结果:整个过程逻辑清晰,步骤明确,最终智能体准确地得出了正确答案“70个”。

4.4 Reflection

在我们已经实现的 ReAct 和 Plan-and-Solve 范式中,智能体一旦完成了任务,其工作流程便告结束。然而,它们生成的初始答案,无论是行动轨迹还是最终结果,都可能存在谬误或有待改进之处。Reflection 机制的核心思想,正是为智能体引入一种事后(post-hoc)的自我校正循环,使其能够像人类一样,审视自己的工作,发现不足,并进行迭代优化。

4.4.1 Reflection 机制的核心思想

Reflection 机制的灵感来源于人类的学习过程:我们完成初稿后会进行校对,解出数学题后会进行验算。这一思想在多个研究中得到了体现,例如 Shinn, Noah 在2023年提出的 Reflexion 框架[3]。其核心工作流程可以概括为一个简洁的三步循环:执行 -> 反思 -> 优化

  1. 执行 (Execution) :首先,智能体使用我们熟悉的方法(如 ReAct 或 Plan-and-Solve)尝试完成任务,生成一个初步的解决方案或行动轨迹。这可以看作是“初稿”。

  2. 反思 (Reflection) :接着,智能体进入反思阶段。它会调用一个独立的、或者带有特殊提示词的大语言模型实例,来扮演一个“评审员”的角色。这个“评审员”会审视第一步生成的“初稿”,并从多个维度进行评估,例如:

    • 事实性错误:是否存在与常识或已知事实相悖的内容?
    • 逻辑漏洞:推理过程是否存在不连贯或矛盾之处?
    • 效率问题:是否有更直接、更简洁的路径来完成任务?
    • 遗漏信息:是否忽略了问题的某些关键约束或方面? 根据评估,它会生成一段结构化的反馈 (Feedback) ,指出具体的问题所在和改进建议。
  3. 优化 (Refinement) :最后,智能体将“初稿”和“反馈”作为新的上下文,再次调用大语言模型,要求它根据反馈内容对初稿进行修正,生成一个更完善的“修订稿”。

如图4.3所示,这个循环可以重复进行多次,直到反思阶段不再发现新的问题,或者达到预设的迭代次数上限。我们可以将这个迭代优化的过程形式化地表达出来。假设 OiOi​ 是第 ii 次迭代产生的输出(O0O0​ 为初始输出),反思模型 πreflectπreflect​ 会生成针对 OiOi​ 的反馈 FiFi​:Fi=πreflect(Task,Oi)Fi​=πreflect​(Task,Oi​)随后,优化模型 πrefineπrefine​ 会结合原始任务、上一版输出以及反馈,生成新一版的输出 Oi+1Oi+1​:Oi+1=πrefine(Task,Oi,Fi)Oi+1​=πrefine​(Task,Oi​,Fi​)

Reflection机制中的“执行-反思-优化”迭代循环

图 4.3 Reflection 机制中的“执行-反思-优化”迭代循环

与前两种范式相比,Reflection 的价值在于:

  • 它为智能体提供了一个内部纠错回路,使其不再完全依赖于外部工具的反馈(ReAct 的 Observation),从而能够修正更高层次的逻辑和策略错误。
  • 它将一次性的任务执行,转变为一个持续优化的过程,显著提升了复杂任务的最终成功率和答案质量。
  • 它为智能体构建了一个临时的 “短期记忆” 。整个“执行-反思-优化”的轨迹形成了一个宝贵的经验记录,智能体不仅知道最终答案,还记得自己是如何从有缺陷的初稿迭代到最终版本的。更进一步,这个记忆系统还可以是多模态的,允许智能体反思和修正文本以外的输出(如代码、图像等),为构建更强大的多模态智能体奠定了基础。

4.4.2 案例设定与记忆模块设计

为了在实战中体现 Reflection 机制,我们将引入记忆管理机制,因为reflection通常对应着信息的存储和提取,如果上下文足够长的情况,想让“评审员”直接获取所有的信息然后进行反思往往会传入很多冗余信息。这一步实践我们主要完成代码生成与迭代优化

这一步的目标任务是:“编写一个Python函数,找出1到n之间所有的素数 (prime numbers)。”

这个任务是检验 Reflection 机制的绝佳场景:

  1. 存在明确的优化路径:大语言模型初次生成的代码很可能是一个简单但效率低下的递归实现。
  2. 反思点清晰:可以通过反思发现其“时间复杂度过高”或“存在重复计算”的问题。
  3. 优化方向明确:可以根据反馈,将其优化为更高效的迭代版本或使用备忘录模式的版本。

Reflection 的核心在于迭代,而迭代的前提是能够记住之前的尝试和获得的反馈。因此,一个“短期记忆”模块是实现该范式的必需品。这个记忆模块将负责存储每一次“执行-反思”循环的完整轨迹。

from typing import List, Dict, Any, Optional

class Memory:
    """
    一个简单的短期记忆模块,用于存储智能体的行动与反思轨迹。
    """

    def __init__(self):
        """
        初始化一个空列表来存储所有记录。
        """
        self.records: List[Dict[str, Any]] = []

    def add_record(self, record_type: str, content: str):
        """
        向记忆中添加一条新记录。

        参数:
        - record_type (str): 记录的类型 ('execution' 或 'reflection')。
        - content (str): 记录的具体内容 (例如,生成的代码或反思的反馈)。
        """
        record = {"type": record_type, "content": content}
        self.records.append(record)
        print(f"📝 记忆已更新,新增一条 '{record_type}' 记录。")

    def get_trajectory(self) -> str:
        """
        将所有记忆记录格式化为一个连贯的字符串文本,用于构建提示词。
        """
        trajectory_parts = []
        for record in self.records:
            if record['type'] == 'execution':
                trajectory_parts.append(f"--- 上一轮尝试 (代码) ---\n{record['content']}")
            elif record['type'] == 'reflection':
                trajectory_parts.append(f"--- 评审员反馈 ---\n{record['content']}")
        
        return "\n\n".join(trajectory_parts)

    def get_last_execution(self) -> Optional[str]:
        """
        获取最近一次的执行结果 (例如,最新生成的代码)。
        如果不存在,则返回 None。
        """
        for record in reversed(self.records):
            if record['type'] == 'execution':
                return record['content']
        return NoneCopy to clipboardErrorCopied

这个 Memory 类的设计比较简洁,主体是这样的:

  • 使用一个列表 records 来按顺序存储每一次的行动和反思。
  • add_record 方法负责向记忆中添加新的条目。
  • get_trajectory 方法是核心,它将记忆轨迹“序列化”成一段文本,可以直接插入到后续的提示词中,为模型的反思和优化提供完整的上下文。
  • get_last_execution 方便我们获取最新的“初稿”以供反思。

4.4.3 Reflection 智能体的编码实现

有了 Memory 模块作为基础,我们现在可以着手构建 ReflectionAgent 的核心逻辑。整个智能体的工作流程将围绕我们之前讨论的“执行-反思-优化”循环展开,并通过精心设计的提示词来引导大语言模型扮演不同的角色。

(1)提示词设计

与之前的范式不同,Reflection 机制需要多个不同角色的提示词来协同工作。

  1. 初始执行提示词 (Execution Prompt)  :这是智能体首次尝试解决问题的提示词,内容相对直接,只要求模型完成指定任务。
INITIAL_PROMPT_TEMPLATE = """
你是一位资深的Python程序员。请根据以下要求,编写一个Python函数。
你的代码必须包含完整的函数签名、文档字符串,并遵循PEP 8编码规范。

要求: {task}

请直接输出代码,不要包含任何额外的解释。
"""Copy to clipboardErrorCopied

2. 反思提示词 (Reflection Prompt)  :这个提示词是 Reflection 机制的灵魂。它指示模型扮演“代码评审员”的角色,对上一轮生成的代码进行批判性分析,并提供具体的、可操作的反馈。

REFLECT_PROMPT_TEMPLATE = """
你是一位极其严格的代码评审专家和资深算法工程师,对代码的性能有极致的要求。
你的任务是审查以下Python代码,并专注于找出其在<strong>算法效率</strong>上的主要瓶颈。

# 原始任务:
{task}

# 待审查的代码:
```python
{code}
```

请分析该代码的时间复杂度,并思考是否存在一种<strong>算法上更优</strong>的解决方案来显著提升性能。
如果存在,请清晰地指出当前算法的不足,并提出具体的、可行的改进算法建议(例如,使用筛法替代试除法)。
如果代码在算法层面已经达到最优,才能回答“无需改进”。

请直接输出你的反馈,不要包含任何额外的解释。
"""Copy to clipboardErrorCopied

3. 优化提示词 (Refinement Prompt)  :当收到反馈后,这个提示词将引导模型根据反馈内容,对原有代码进行修正和优化。


REFINE_PROMPT_TEMPLATE = """
你是一位资深的Python程序员。你正在根据一位代码评审专家的反馈来优化你的代码。

# 原始任务:
{task}

# 你上一轮尝试的代码:
{last_code_attempt}
评审员的反馈:
{feedback}

请根据评审员的反馈,生成一个优化后的新版本代码。
你的代码必须包含完整的函数签名、文档字符串,并遵循PEP 8编码规范。
请直接输出优化后的代码,不要包含任何额外的解释。
"""Copy to clipboardErrorCopied

(2)智能体封装与实现

现在,我们将这套提示词逻辑和 Memory 模块整合到 ReflectionAgent 类中。

# 假设 llm_client.py 和 memory.py 已定义
# from llm_client import HelloAgentsLLM
# from memory import Memory

class ReflectionAgent:
    def __init__(self, llm_client, max_iterations=3):
        self.llm_client = llm_client
        self.memory = Memory()
        self.max_iterations = max_iterations

    def run(self, task: str):
        print(f"\n--- 开始处理任务 ---\n任务: {task}")

        # --- 1. 初始执行 ---
        print("\n--- 正在进行初始尝试 ---")
        initial_prompt = INITIAL_PROMPT_TEMPLATE.format(task=task)
        initial_code = self._get_llm_response(initial_prompt)
        self.memory.add_record("execution", initial_code)

        # --- 2. 迭代循环:反思与优化 ---
        for i in range(self.max_iterations):
            print(f"\n--- 第 {i+1}/{self.max_iterations} 轮迭代 ---")

            # a. 反思
            print("\n-> 正在进行反思...")
            last_code = self.memory.get_last_execution()
            reflect_prompt = REFLECT_PROMPT_TEMPLATE.format(task=task, code=last_code)
            feedback = self._get_llm_response(reflect_prompt)
            self.memory.add_record("reflection", feedback)

            # b. 检查是否需要停止
            if "无需改进" in feedback:
                print("\n✅ 反思认为代码已无需改进,任务完成。")
                break

            # c. 优化
            print("\n-> 正在进行优化...")
            refine_prompt = REFINE_PROMPT_TEMPLATE.format(
                task=task,
                last_code_attempt=last_code,
                feedback=feedback
            )
            refined_code = self._get_llm_response(refine_prompt)
            self.memory.add_record("execution", refined_code)
        
        final_code = self.memory.get_last_execution()
        print(f"\n--- 任务完成 ---\n最终生成的代码:\n```python\n{final_code}\n```")
        return final_code

    def _get_llm_response(self, prompt: str) -> str:
        """一个辅助方法,用于调用LLM并获取完整的流式响应。"""
        messages = [{"role": "user", "content": prompt}]
        response_text = self.llm_client.think(messages=messages) or ""
        return response_text
Copy to clipboardErrorCopied

4.4.4 运行实例与分析

完整的代码同样参考本书配套的代码仓库 code 文件夹,这里提供一个输出实例。

--- 开始处理任务 ---
任务: 编写一个Python函数,找出1到n之间所有的素数 (prime numbers)。

--- 正在进行初始尝试 ---
🧠 正在调用 xxxxxx 模型...
✅ 大语言模型响应成功:
```python
def find_primes(n):
    ...
    return primes
```
📝 记忆已更新,新增一条 'execution' 记录。   

--- 第 1/2 轮迭代 ---

-> 正在进行反思...
🧠 正在调用 xxxxxx 模型...
✅ 大语言模型响应成功:
当前代码的时间复杂度为O(n * sqrt(n))。虽然对于较小的n值,这种实现是可以接受的,但当n非常大时,性能会显著下降。主要瓶颈在于每个数都需要进行试除法检查,这导致了较高的时间开销。

建议使用埃拉托斯特尼筛法(Sieve of Eratosthenes),该算法的时间复杂度为O(n log(log n)),能够显著提高查找素数的效率。

改进后的代码如下:
```python
def find_primes(n):
    ...
    return primes
```
📝 记忆已更新,新增一条 'reflection' 记录。

-> 正在进行优化...
🧠 正在调用 xxxxxx 模型...
✅ 大语言模型响应成功:
```python
def find_primes(n):
    ...
    return primes
```
📝 记忆已更新,新增一条 'execution' 记录。

--- 第 2/2 轮迭代 ---

-> 正在进行反思...
🧠 正在调用 xxxxxx 模型...
✅ 大语言模型响应成功:
当前代码使用了Eratosthenes筛法,时间复杂度为O(n log log n),空间复杂度为O(n)。此算法在寻找1到n之间的所有素数时已经非常高效,通常情况下无需进一步优化。但在某些特定场景下,可以考虑以下改进:

1. <strong>分段筛法(Segmented Sieve)</strong>:适用于n非常大但内存有限的情况。将区间分成多个小段,每段分别用筛法处理,减少内存使用。
2. <strong>奇数筛法(Odd Number Sieve)</strong>:除了2以外,所有素数都是奇数。可以在初始化`is_prime`数组时只标记奇数,这样可以将空间复杂度降低一半,同时减少一些不必要的计算。

然而,这些改进对于大多数应用场景来说并不是必需的,因为标准的Eratosthenes筛法已经足够高效。因此,在一般情况下,<strong>无需改进</strong>。
📝 记忆已更新,新增一条 'reflection' 记录。

✅ 反思认为代码已无需改进,任务完成。

--- 任务完成 ---
最终生成的代码:
```python
def find_primes(n):
    """
    Finds all prime numbers between 1 and n using the Sieve of Eratosthenes algorithm.

    :param n: The upper limit of the range to find prime numbers.
    :return: A list of all prime numbers between 1 and n.
    """
    if n < 2:
        return []

    is_prime = [True] * (n + 1)
    is_prime[0] = is_prime[1] = False

    p = 2
    while p * p <= n:
        if is_prime[p]:
            for i in range(p * p, n + 1, p):
                is_prime[i] = False
        p += 1

    primes = [num for num in range(2, n + 1) if is_prime[num]]
    return primes
```Copy to clipboardErrorCopied

这个运行实例展示了 Reflection 机制是如何驱动智能体进行深度优化的:

  1. 有效的“批判”是优化的前提:在第一轮反思中,由于我们使用了“极其严格”且“专注于算法效率”的提示词,智能体没有满足于功能正确的初版代码,而是精准地指出了其 O(n * sqrt(n)) 的时间复杂度瓶颈,并提出了算法层面的改进建议——埃拉托斯特尼筛法。
  2. 迭代式改进: 智能体在接收到明确的反馈后,于优化阶段成功地实现了更高效的筛法,将算法复杂度降至 O(n log log n),完成了第一次有意义的自我迭代。
  3. 收敛与终止: 在第二轮反思中,智能体面对已经高效的筛法,展现出了更深层次的知识。它不仅肯定了当前算法的效率,甚至还提及了分段筛法等更高级的优化方向,但最终做出了“在一般情况下无需改进”的正确判断。这个判断触发了我们的终止条件,使优化过程得以收敛。

这个案例充分证明,一个设计良好的 Reflection 机制,其价值不仅在于修复错误,更在于驱动解决方案在质量和效率上实现阶梯式的提升,这使其成为构建复杂、高质量智能体的关键技术之一。

4.4.5 Reflection 机制的成本收益分析

尽管 Reflection 机制在提升任务解决质量上表现出色,但这种能力的获得并非没有代价。在实际应用中,我们需要权衡其带来的收益与相应的成本。

(1)主要成本

  1. 模型调用开销增加:这是最直接的成本。每进行一轮迭代,至少需要额外调用两次大语言模型(一次用于反思,一次用于优化)。如果迭代多轮,API 调用成本和计算资源消耗将成倍增加。
  2. 任务延迟显著提高:Reflection 是一个串行过程,每一轮的优化都必须等待上一轮的反思完成。这使得任务的总耗时显著延长,不适合对实时性要求高的场景。
  3. 提示工程复杂度上升:如我们的案例所示,Reflection 的成功在很大程度上依赖于高质量、有针对性的提示词。为“执行”、“反思”、“优化”等不同阶段设计和调试有效的提示词,需要投入更多的开发精力。

(2)核心收益

  1. 解决方案质量的跃迁:最大的收益在于,它能将一个“合格”的初始方案,迭代优化成一个“优秀”的最终方案。这种从功能正确到性能高效、从逻辑粗糙到逻辑严谨的提升,在很多关键任务中是至关重要的。
  2. 鲁棒性与可靠性增强:通过内部的自我纠错循环,智能体能够发现并修复初始方案中可能存在的逻辑漏洞、事实性错误或边界情况处理不当等问题,从而大大提高了最终结果的可靠性。

综上所述,Reflection 机制是一种典型的“以成本换质量”的策略。它非常适合那些对最终结果的质量、准确性和可靠性有极高要求,且对任务完成的实时性要求相对宽松的场景。例如:

  • 生成关键的业务代码或技术报告。
  • 在科学研究中进行复杂的逻辑推演。
  • 需要深度分析和规划的决策支持系统。

反之,如果应用场景需要快速响应,或者一个“大致正确”的答案就已经足够,那么使用更轻量的 ReAct 或 Plan-and-Solve 范式可能会是更具性价比的选择。

4.5 本章小结

在本章中,以第三章掌握的大语言模型知识为基础,我们通过“亲手造轮子”的方式,从零开始编码实现了三种业界经典的智能体构建范式:ReAct、Plan-and-Solve 与 Reflection。我们不仅探索了它们的核心工作原理,还通过具体的实战案例,深入了解了各自的优势、局限与适用场景。

核心知识点回顾:

  1. ReAct:我们构建了一个能与外部世界交互的 ReAct 智能体。通过“思考-行动-观察”的动态循环,它成功地利用搜索引擎回答了自身知识库无法覆盖的实时性问题。其核心优势在于环境适应性动态纠错能力,使其成为处理探索性、需要外部工具输入的任务的首选。
  2. Plan-and-Solve:我们实现了一个先规划后执行的 Plan-and-Solve 智能体,并利用它解决了需要多步推理的数学应用题。它将复杂的任务分解为清晰的步骤,然后逐一执行。其核心优势在于结构性稳定性,特别适合处理逻辑路径确定、内部推理密集的任务。
  3. Reflection (自我反思与迭代):我们构建了一个具备自我优化能力的 Reflection 智能体。通过引入“执行-反思-优化”的迭代循环,它成功地将一个效率较低的初始代码方案,优化为了一个算法上更优的高性能版本。其核心价值在于能显著提升解决方案的质量,适用于对结果的准确性和可靠性有极高要求的场景。

本章探讨的三种范式,代表了智能体解决问题的三种不同策略,如表4.1所示。在实际应用中,选择哪一种,取决于任务的核心需求:

表 4.1 不同 Agent Loop 的选择策略

至此,我们已经掌握了构建单个智能体的核心技术。为了过渡知识,以及对实际应用更加深入。下一节我们将会探索不同低代码平台的使用方式以及轻代码构建agent的方案。

第三章 大语言模型基础

前两章分别介绍了智能体的定义和发展历史,本章将完全聚焦于大语言模型本身解答一个关键问题:现代智能体是如何工作的?我们将从语言模型的基本定义出发,通过对这些原理的学习,为理解LLM如何获得强大的知识储备与推理能力打下坚实的基础。

3.1 语言模型与 Transformer 架构

3.1.1 从 N-gram 到 RNN

语言模型 (Language Model, LM)  是自然语言处理的核心,其根本任务是计算一个词序列(即一个句子)出现的概率。一个好的语言模型能够告诉我们什么样的句子是通顺的、自然的。在多智能体系统中,语言模型是智能体理解人类指令、生成回应的基础。本节将回顾从经典的统计方法到现代深度学习模型的演进历程,为理解后续的 Transformer 架构打下坚实的基础。

(1)统计语言模型与N-gram的思想

在深度学习兴起之前,统计方法是语言模型的主流。其核心思想是,一个句子出现的概率,等于该句子中每个词出现的条件概率的连乘。对于一个由词 w1,w2,⋯ ,wmw1​,w2​,⋯,wm​ 构成的句子 S,其概率 P(S) 可以表示为:

P(S)=P(w1,w2,…,wm)=P(w1)⋅P(w2∣w1)⋅P(w3∣w1,w2)⋯P(wm∣w1,…,wm−1)P(S)=P(w1​,w2​,…,wm​)=P(w1​)⋅P(w2​∣w1​)⋅P(w3​∣w1​,w2​)⋯P(wm​∣w1​,…,wm−1​)

这个公式被称为概率的链式法则。然而,直接计算这个公式几乎是不可能的,因为像 P(wm∣w1,⋯ ,wm−1)P(wm​∣w1​,⋯,wm−1​) 这样的条件概率太难从语料库中估计了,词序列 w1,⋯ ,wm−1w1​,⋯,wm−1​ 可能从未在训练数据中出现过。

图片描述

图 3.1 马尔可夫假设示意图

为了解决这个问题,研究者引入了马尔可夫假设 (Markov Assumption)  。其核心思想是:我们不必回溯一个词的全部历史,可以近似地认为,一个词的出现概率只与它前面有限的 n−1n−1 个词有关,如图3.1所示。基于这个假设建立的语言模型,我们称之为 N-gram模型。这里的 "N" 代表我们考虑的上下文窗口大小。让我们来看几个最常见的例子来理解这个概念:

  • Bigram (当 N=2 时)  :这是最简单的情况,我们假设一个词的出现只与它前面的一个词有关。因此,链式法则中复杂的条件概率 P(wi∣w1,⋯ ,wi−1)P(wi​∣w1​,⋯,wi−1​) 就可以被近似为更容易计算的形式:

P(wi∣w1,…,wi−1)≈P(wi∣wi−1)P(wi​∣w1​,…,wi−1​)≈P(wi​∣wi−1​)

  • Trigram (当 N=3 时)  :类似地,我们假设一个词的出现只与它前面的两个词有关:

P(wi∣w1,…,wi−1)≈P(wi∣wi−2,wi−1)P(wi​∣w1​,…,wi−1​)≈P(wi​∣wi−2​,wi−1​)

这些概率可以通过在大型语料库中进行最大似然估计(Maximum Likelihood Estimation,MLE)  来计算。这个术语听起来很复杂,但其思想非常直观:最可能出现的,就是我们在数据中看到次数最多的。例如,对于 Bigram 模型,我们想计算在词 wi−1wi−1​ 出现后,下一个词是 wiwi​ 的概率 P(wi∣wi−1)P(wi​∣wi−1​)。根据最大似然估计,这个概率可以通过简单的计数来估算:

P(wi∣wi−1)=Count(wi−1,wi)Count(wi−1)P(wi​∣wi−1​)=Count(wi−1​)Count(wi−1​,wi​)​

这里的 Count() 函数就代表“计数”:

  • Count(wi−1,wi)Count(wi​−1,wi​):表示词对 (wi−1,wi)(wi−1​,wi​) 在语料库中连续出现的总次数。
  • Count(wi−1)Count(wi−1​):表示单个词 wi−1wi−1​ 在语料库中出现的总次数。

公式的含义就是:我们用“词对 Count(wi−1,wi)Count(wi​−1,wi​) 出现的次数”除以“词 Count(wi−1)Count(wi−1​) 出现的总次数”,来作为 P(wi∣wi−1)P(wi​∣wi−1​) 的一个近似估计。

为了让这个过程更具体,我们来手动进行一次计算。假设我们拥有一个仅包含以下两句话的迷你语料库:datawhale agent learnsdatawhale agent works。我们的目标是:使用 Bigram (N=2) 模型,估算句子 datawhale agent learns 出现的概率。根据 Bigram 的假设,我们每次会考察连续的两个词(即一个词对)。

第一步:计算第一个词的概率 P(datawhale)P(datawhale) 这是 datawhale 出现的次数除以总词数。datawhale 出现了 2 次,总词数是 6。

P(datawhale)=总语料中"datawhale"的数量总语料的词数=26≈0.333P(datawhale)=总语料的词数总语料中"datawhale"的数量​=62​≈0.333

第二步:计算条件概率 P(agent∣datawhale)P(agent∣datawhale) 这是词对 datawhale agent 出现的次数除以 datawhale 出现的总次数。datawhale agent 出现了 2 次,datawhale 出现了 2 次。

P(agent∣datawhale)=Count(datawhale agent)Count(datawhale)=22=1P(agent∣datawhale)=Count(datawhale)Count(datawhale agent)​=22​=1

第三步:计算条件概率 P(learns∣agent)P(learns∣agent) 这是词对 agent learns 出现的次数除以 agent 出现的总次数。agent learns 出现了 1 次,agent 出现了 2 次。

P(learns∣agent)=Count(agent learns)Count(agent)=12=0.5P(learns∣agent)=Count(agent)Count(agent learns)​=21​=0.5

最后:将概率连乘 所以,整个句子的近似概率为:

P(datawhale agent learns)≈P(datawhale)⋅P(agent∣datawhale)⋅P(learns∣agent)≈0.333⋅1⋅0.5≈0.167P(datawhale agent learns)≈P(datawhale)⋅P(agent∣datawhale)⋅P(learns∣agent)≈0.333⋅1⋅0.5≈0.167

import collections

# 示例语料库,与上方案例讲解中的语料库保持一致
corpus = "datawhale agent learns datawhale agent works"
tokens = corpus.split()
total_tokens = len(tokens)

# --- 第一步:计算 P(datawhale) ---
count_datawhale = tokens.count('datawhale')
p_datawhale = count_datawhale / total_tokens
print(f"第一步: P(datawhale) = {count_datawhale}/{total_tokens} = {p_datawhale:.3f}")

# --- 第二步:计算 P(agent|datawhale) ---
# 先计算 bigrams 用于后续步骤
bigrams = zip(tokens, tokens[1:])
bigram_counts = collections.Counter(bigrams)
count_datawhale_agent = bigram_counts[('datawhale', 'agent')]
# count_datawhale 已在第一步计算
p_agent_given_datawhale = count_datawhale_agent / count_datawhale
print(f"第二步: P(agent|datawhale) = {count_datawhale_agent}/{count_datawhale} = {p_agent_given_datawhale:.3f}")

# --- 第三步:计算 P(learns|agent) ---
count_agent_learns = bigram_counts[('agent', 'learns')]
count_agent = tokens.count('agent')
p_learns_given_agent = count_agent_learns / count_agent
print(f"第三步: P(learns|agent) = {count_agent_learns}/{count_agent} = {p_learns_given_agent:.3f}")

# --- 最后:将概率连乘 ---
p_sentence = p_datawhale * p_agent_given_datawhale * p_learns_given_agent
print(f"最后: P('datawhale agent learns') ≈ {p_datawhale:.3f} * {p_agent_given_datawhale:.3f} * {p_learns_given_agent:.3f} = {p_sentence:.3f}")

>>>
第一步: P(datawhale) = 2/6 = 0.333
第二步: P(agent|datawhale) = 2/2 = 1.000
第三步: P(learns|agent) = 1/2 = 0.500
最后: P('datawhale agent learns') ≈ 0.333 * 1.000 * 0.500 = 0.167Copy to clipboardErrorCopied

N-gram 模型虽然简单有效,但有两个致命缺陷:

  1. 数据稀疏性 (Sparsity)  :如果一个词序列从未在语料库中出现,其概率估计就为 0,这显然是不合理的。虽然可以通过平滑 (Smoothing) 技术缓解,但无法根除。
  2. 泛化能力差: 模型无法理解词与词之间的语义相似性。例如,即使模型在语料库中见过很多次 agent learns,它也无法将这个知识泛化到语义相似的词上。当我们计算 robot learns 的概率时,如果 robot 这个词从未出现过,或者 robot learns 这个组合从未出现过,模型计算出的概率也会是零。模型无法理解 agent 和 robot 在语义上的相似性。

(2)神经网络语言模型与词嵌入

N-gram 模型的根本缺陷在于它将词视为孤立、离散的符号。为了克服这个问题,研究者们转向了神经网络,并提出了一种思想:用连续的向量来表示词。2003年,Bengio 等人提出的前馈神经网络语言模型 (Feedforward Neural Network Language Model)  是这一领域的里程碑[1]。

其核心思想可以分为两步:

  1. 构建一个语义空间:创建一个高维的连续向量空间,然后将词汇表中的每个词都映射为该空间中的一个点。这个点(即向量)就被称为词嵌入 (Word Embedding)  或词向量。在这个空间里,语义上相近的词,它们对应的向量在空间中的位置也相近。例如,agent 和 robot 的向量会靠得很近,而 agent 和 apple 的向量会离得很远。
  2. 学习从上下文到下一个词的映射:利用神经网络的强大拟合能力,来学习一个函数。这个函数的输入是前 n−1n−1 个词的词向量,输出是词汇表中每个词在当前上下文后出现的概率分布。

图片描述

图 3.2 神经网络语言模型架构示意图

如图3.2所示,在这个架构中,词嵌入是在模型训练过程中自动学习得到的。模型为了完成“预测下一个词”这个任务,会不断调整每个词的向量位置,最终使这些向量能够蕴含丰富的语义信息。一旦我们将词转换成了向量,我们就可以用数学工具来度量它们之间的关系。最常用的方法是余弦相似度 (Cosine Similarity)  ,它通过计算两个向量夹角的余弦值来衡量它们的相似性。

similarity(a⃗,b⃗)=cos⁡(θ)=a⃗⋅b⃗∣a⃗∣∣b⃗∣similarity(a,b)=cos(θ)=∣a∣∣b∣a⋅b​

这个公式的含义是:

  • 如果两个向量方向完全相同,夹角为0°,余弦值为1,表示完全相关。
  • 如果两个向量方向正交,夹角为90°,余弦值为0,表示毫无关系。
  • 如果两个向量方向完全相反,夹角为180°,余弦值为-1,表示完全负相关。

通过这种方式,词向量不仅能捕捉到“同义词”这类简单的关系,还能捕捉到更复杂的类比关系。

一个著名的例子展示了词向量捕捉到的语义关系: vector('King') - vector('Man') + vector('Woman') 这个向量运算的结果,在向量空间中与 vector('Queen') 的位置惊人地接近。这好比在进行语义的平移:我们从“国王”这个点出发,减去“男性”的向量,再加上“女性”的向量,最终就抵达了“女王”的位置。这证明了词嵌入能够学习到“性别”、“皇室”这类抽象概念。

import numpy as np

# 假设我们已经学习到了简化的二维词向量
embeddings = {
    "king": np.array([0.9, 0.8]),
    "queen": np.array([0.9, 0.2]),
    "man": np.array([0.7, 0.9]),
    "woman": np.array([0.7, 0.3])
}

def cosine_similarity(vec1, vec2):
    dot_product = np.dot(vec1, vec2)
    norm_product = np.linalg.norm(vec1) * np.linalg.norm(vec2)
    return dot_product / norm_product

# king - man + woman
result_vec = embeddings["king"] - embeddings["man"] + embeddings["woman"]

# 计算结果向量与 "queen" 的相似度
sim = cosine_similarity(result_vec, embeddings["queen"])

print(f"king - man + woman 的结果向量: {result_vec}")
print(f"该结果与 'queen' 的相似度: {sim:.4f}")

>>>
king - man + woman 的结果向量: [0.9 0.2]
该结果与 'queen' 的相似度: 1.0000Copy to clipboardErrorCopied

神经网络语言模型通过词嵌入,成功解决了 N-gram 模型的泛化能力差的问题。然而,它仍然有一个类似 N-gram 的限制:上下文窗口是固定的。它只能考虑固定数量的前文,这为能处理任意长序列的循环神经网络埋下了伏笔。

(3)循环神经网络 (RNN) 与长短时记忆网络 (LSTM)

前一节的神经网络语言模型虽然引入了词嵌入解决了泛化问题,但它和 N-gram 模型一样,上下文窗口是固定大小的。为了预测下一个词,它只能看到前 n−1 个词,再早的历史信息就被丢弃了。这显然不符合我们人类理解语言的方式。为了打破固定窗口的限制,循环神经网络 (Recurrent Neural Network, RNN)  应运而生,其核心思想非常直观:为网络增加“记忆”能力[2]。

如图3.3所示,RNN 的设计引入了一个隐藏状态 (hidden state)  向量,我们可以将其理解为网络的短期记忆。在处理序列的每一步,网络都会读取当前的输入词,并结合它上一刻的记忆(即上一个时间步的隐藏状态),然后生成一个新的记忆(即当前时间步的隐藏状态)传递给下一刻。这个循环往复的过程,使得信息可以在序列中不断向后传递。

图片描述

图 3.3 RNN 结构示意图

然而,标准的 RNN 在实践中存在一个严重的问题:长期依赖问题 (Long-term Dependency Problem)  。在训练过程中,模型需要通过反向传播算法根据输出端的误差来调整网络深处的权重。对于 RNN 而言,序列的长度就是网络的深度。当序列很长时,梯度在从后向前传播的过程中会经过多次连乘,这会导致梯度值快速趋向于零(梯度消失)或变得极大(梯度爆炸)。梯度消失使得模型无法有效学习到序列早期信息对后期输出的影响,即难以捕捉长距离的依赖关系。

为了解决长期依赖问题,长短时记忆网络 (Long Short-Term Memory, LSTM)  被设计出来[3]。LSTM 是一种特殊的 RNN,其核心创新在于引入了细胞状态 (Cell State)  和一套精密的门控机制 (Gating Mechanism)  。细胞状态可以看作是一条独立于隐藏状态的信息通路,允许信息在时间步之间更顺畅地传递。门控机制则是由几个小型神经网络构成,它们可以学习如何有选择地让信息通过,从而控制细胞状态中信息的增加与移除。这些门包括:

  • 遗忘门 (Forget Gate) :决定从上一时刻的细胞状态中丢弃哪些信息。
  • 输入门 (Input Gate) :决定将当前输入中的哪些新信息存入细胞状态。
  • 输出门 (Output Gate) :决定根据当前的细胞状态,输出哪些信息到隐藏状态。

3.1.2 Transformer 架构解析

在上一节中,我们看到RNN及LSTM通过引入循环结构来处理序列数据,这在一定程度上解决了捕捉长距离依赖的问题。然而,这种循环的计算方式也带来了新的瓶颈:它必须按顺序处理数据。第 t 个时间步的计算,必须等待第 t−1 个时间步完成后才能开始。这意味着 RNN 无法进行大规模的并行计算,在处理长序列时效率低下,这极大地限制了模型规模和训练速度的提升。Transformer在2017 年由谷歌团队提出[4]。它完全抛弃了循环结构,转而完全依赖一种名为注意力 (Attention)  的机制来捕捉序列内的依赖关系,从而实现了真正意义上的并行计算。

(1)Encoder-Decoder 整体结构

最初的 Transformer 模型是为端到端任务机器翻译而设计的。如图3.4所示,它在宏观上遵循了一个经典的编码器-解码器 (Encoder-Decoder)  架构。

图片描述

图 3.4 Transformer 整体架构图

我们可以将这个结构理解为一个分工明确的团队:

  1. 编码器 (Encoder)  :任务是“理解”输入的整个句子。它会读取所有输入词元(这个概念会在3.2.2节介绍),最终为每个词元生成一个富含上下文信息的向量表示。
  2. 解码器 (Decoder)  :任务是“生成”目标句子。它会参考自己已经生成的前文,并“咨询”编码器的理解结果,来生成下一个词。

为了真正理解 Transformer 的工作原理,最好的方法莫过于亲手实现它。在本节中,我们将采用一种“自顶向下”的方法:首先,我们搭建出 Transformer 完整的代码框架,定义好所有需要的类和方法。然后,我们将像完成拼图一样,逐一实现这些类的具体功能。

import torch
import torch.nn as nn
import math

# --- 占位符模块,将在后续小节中实现 ---

class PositionalEncoding(nn.Module):
    """
    位置编码模块
    """
    def forward(self, x):
        pass

class MultiHeadAttention(nn.Module):
    """
    多头注意力机制模块
    """
    def forward(self, query, key, value, mask):
        pass

class PositionWiseFeedForward(nn.Module):
    """
    位置前馈网络模块
    """
    def forward(self, x):
        pass

# --- 编码器核心层 ---

class EncoderLayer(nn.Module):
    def __init__(self, d_model, num_heads, d_ff, dropout):
        super(EncoderLayer, self).__init__()
        self.self_attn = MultiHeadAttention() # 待实现
        self.feed_forward = PositionWiseFeedForward() # 待实现
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, mask):
        # 残差连接与层归一化将在 3.1.2.4 节中详细解释
        # 1. 多头自注意力
        attn_output = self.self_attn(x, x, x, mask)
        x = self.norm1(x + self.dropout(attn_output))

        # 2. 前馈网络
        ff_output = self.feed_forward(x)
        x = self.norm2(x + self.dropout(ff_output))

        return x

# --- 解码器核心层 ---

class DecoderLayer(nn.Module):
    def __init__(self, d_model, num_heads, d_ff, dropout):
        super(DecoderLayer, self).__init__()
        self.self_attn = MultiHeadAttention() # 待实现
        self.cross_attn = MultiHeadAttention() # 待实现
        self.feed_forward = PositionWiseFeedForward() # 待实现
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.norm3 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, encoder_output, src_mask, tgt_mask):
        # 1. 掩码多头自注意力 (对自己)
        attn_output = self.self_attn(x, x, x, tgt_mask)
        x = self.norm1(x + self.dropout(attn_output))

        # 2. 交叉注意力 (对编码器输出)
        cross_attn_output = self.cross_attn(x, encoder_output, encoder_output, src_mask)
        x = self.norm2(x + self.dropout(cross_attn_output))

        # 3. 前馈网络
        ff_output = self.feed_forward(x)
        x = self.norm3(x + self.dropout(ff_output))

        return xCopy to clipboardErrorCopied

(2)从自注意力到多头注意力

现在,我们来填充骨架中最关键的模块,注意力机制。

想象一下我们阅读这个句子:“The agent learns because it is intelligent.”。当我们读到加粗的 "it" 时,为了理解它的指代,我们的大脑会不自觉地将更多的注意力放在前面的 "agent" 这个词上。自注意力 (Self-Attention)  机制就是对这种现象的数学建模。它允许模型在处理序列中的每一个词时,都能兼顾句子中的所有其他词,并为这些词分配不同的“注意力权重”。权重越高的词,代表其与当前词的关联性越强,其信息也应该在当前词的表示中占据更大的比重。

为了实现上述过程,自注意力机制为每个输入的词元向量引入了三个可学习的角色:

  • 查询 (Query, Q) :代表当前词元,它正在主动地“查询”其他词元以获取信息。
  • 键 (Key, K) :代表句子中可被查询的词元“标签”或“索引”。
  • 值 (Value, V) :代表词元本身所携带的“内容”或“信息”。

这三个向量都是由原始的词嵌入向量乘以三个不同的、可学习的权重矩阵 (WQ,WK,WVWQ,WK,WV) 得到的。整个计算过程可以分为以下几步,我们可以把它想象成一次高效的开卷考试:

  • 准备“考题”和“资料”:对于句子中的每个词,都通过权重矩阵生成其Q,K,VQ,K,V向量。
  • 计算相关性得分:要计算词AA的新表示,就用词AA的QQ向量,去和句子中所有词(包括AA自己)的KK向量进行点积运算。这个得分反映了其他词对于理解词AA的重要性。
  • 稳定化与归一化:将得到的所有分数除以一个缩放因子dkdk​​(dkdk​是KK向量的维度),以防止梯度过小,然后用Softmax函数将分数转换成总和为1的权重,也就是归一化的过程。
  • 加权求和:将上一步得到的权重分别乘以每个词对应的VV向量,然后将所有结果相加。最终得到的向量,就是词AA融合了全局上下文信息后的新表示。

这个过程可以用一个简洁的公式来概括:

Attention(Q,K,V)=softmax(QKTdk)VAttention(Q,K,V)=softmax(dk​​QKT​)V

如果只进行一次上述的注意力计算(即单头),模型可能会只学会关注一种类型的关联。比如,在处理 "it" 时,可能只学会了关注主语。但语言中的关系是复杂的,我们希望模型能同时关注多种关系(如指代关系、时态关系、从属关系等)。多头注意力机制应运而生。它的思想很简单:把一次做完变成分成几组,分开做,再合并。

它将原始的 Q, K, V 向量在维度上切分成 h 份(h 就是“头”数),每一份都独立地进行一次单头注意力的计算。这就好比让 h 个不同的“专家”从不同的角度去审视句子,每个专家都能捕捉到一种不同的特征关系。最后,将这 h 个专家的“意见”(即输出向量)拼接起来,再通过一个线性变换进行整合,就得到了最终的输出。

图片描述

图 3.5 多头注意力机制

如图3.5所示,这种设计让模型能够共同关注来自不同位置、不同表示子空间的信息,极大地增强了模型的表达能力。以下是多头注意力的简单实现可供参考。

class MultiHeadAttention(nn.Module):
    """
    多头注意力机制模块
    """
    def __init__(self, d_model, num_heads):
        super(MultiHeadAttention, self).__init__()
        assert d_model % num_heads == 0, "d_model 必须能被 num_heads 整除"

        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model // num_heads

        # 定义 Q, K, V 和输出的线性变换层
        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, d_model)
        self.W_v = nn.Linear(d_model, d_model)
        self.W_o = nn.Linear(d_model, d_model)

    def scaled_dot_product_attention(self, Q, K, V, mask=None):
        # 1. 计算注意力得分 (QK^T)
        attn_scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)

        # 2. 应用掩码 (如果提供)
        if mask is not None:
            # 将掩码中为 0 的位置设置为一个非常小的负数,这样 softmax 后会接近 0
            attn_scores = attn_scores.masked_fill(mask == 0, -1e9)

        # 3. 计算注意力权重 (Softmax)
        attn_probs = torch.softmax(attn_scores, dim=-1)

        # 4. 加权求和 (权重 * V)
        output = torch.matmul(attn_probs, V)
        return output

    def split_heads(self, x):
        # 将输入 x 的形状从 (batch_size, seq_length, d_model)
        # 变换为 (batch_size, num_heads, seq_length, d_k)
        batch_size, seq_length, d_model = x.size()
        return x.view(batch_size, seq_length, self.num_heads, self.d_k).transpose(1, 2)

    def combine_heads(self, x):
        # 将输入 x 的形状从 (batch_size, num_heads, seq_length, d_k)
        # 变回 (batch_size, seq_length, d_model)
        batch_size, num_heads, seq_length, d_k = x.size()
        return x.transpose(1, 2).contiguous().view(batch_size, seq_length, self.d_model)

    def forward(self, Q, K, V, mask=None):
        # 1. 对 Q, K, V 进行线性变换
        Q = self.split_heads(self.W_q(Q))
        K = self.split_heads(self.W_k(K))
        V = self.split_heads(self.W_v(V))

        # 2. 计算缩放点积注意力
        attn_output = self.scaled_dot_product_attention(Q, K, V, mask)

        # 3. 合并多头输出并进行最终的线性变换
        output = self.W_o(self.combine_heads(attn_output))
        return outputCopy to clipboardErrorCopied

(3)前馈神经网络

在每个 Encoder 和 Decoder 层中,多头注意力子层之后都跟着一个逐位置前馈网络(Position-wise Feed-Forward Network, FFN)  。如果说注意力层的作用是从整个序列中“动态地聚合”相关信息,那么前馈网络的作用从这些聚合后的信息中提取更高阶的特征。

这个名字的关键在于“逐位置”。它意味着这个前馈网络会独立地作用于序列中的每一个词元向量。换句话说,对于一个长度为 seq_len 的序列,这个 FFN 实际上会被调用 seq_len 次,每次处理一个词元。重要的是,所有位置共享的是同一组网络权重。这种设计既保持了对每个位置进行独立加工的能力,又大大减少了模型的参数量。这个网络的结构非常简单,由两个线性变换和一个 ReLU 激活函数组成:

FFN(x)=max⁡(0,xW1+b1)W2+b2FFN(x)=max(0,xW1​+b1​)W2​+b2​

其中,xx是注意力子层的输出。 W1,b1,W2,b2W1​,b1​,W2​,b2​是可学习的参数。通常,第一个线性层的输出维度 d_ff 会远大于输入的维度 d_model(例如 d_ff = 4 * d_model),经过 ReLU 激活后再通过第二个线性层映射回 d_model 维度。这种“先扩大再缩小”的模式,被认为有助于模型学习更丰富的特征表示。

在我们的 PyTorch 骨架中,我们可以用以下代码来实现这个模块:

class PositionWiseFeedForward(nn.Module):
    """
    位置前馈网络模块
    """
    def __init__(self, d_model, d_ff, dropout=0.1):
        super(PositionWiseFeedForward, self).__init__()
        self.linear1 = nn.Linear(d_model, d_ff)
        self.dropout = nn.Dropout(dropout)
        self.linear2 = nn.Linear(d_ff, d_model)
        self.relu = nn.ReLU()

    def forward(self, x):
        # x 形状: (batch_size, seq_len, d_model)
        x = self.linear1(x)
        x = self.relu(x)
        x = self.dropout(x)
        x = self.linear2(x)
        # 最终输出形状: (batch_size, seq_len, d_model)
        return xCopy to clipboardErrorCopied

(4)残差连接与层归一化

在 Transformer 的每个编码器和解码器层中,所有子模块(如多头注意力和前馈网络)都被一个 Add & Norm 操作包裹。这个组合是为了保证 Transformer 能够稳定训练。

这个操作由两个部分组成:

  • 残差连接 (Add) :该操作将子模块的输入 x 直接加到该子模块的输出 Sublayer(x) 上。这一结构解决了深度神经网络中的梯度消失 (Vanishing Gradients)  问题。在反向传播时,梯度可以绕过子模块直接向前传播,从而保证了即使网络层数很深,模型也能得到有效的训练。其公式可以表示为:Output=x+Sublayer(x)Output=x+Sublayer(x)。
  • 层归一化 (Norm) :该操作对单个样本的所有特征进行归一化,使其均值为0,方差为1。这解决了模型训练过程中的内部协变量偏移 (Internal Covariate Shift)  问题,使每一层的输入分布保持稳定,从而加速模型收敛并提高训练的稳定性。

3.1.2.5 位置编码

我们已经了解,Transformer 的核心是自注意力机制,它通过计算序列中任意两个词元之间的关系来捕捉依赖。然而,这种计算方式有一个固有的问题:它本身不包含任何关于词元顺序或位置的信息。对于自注意力来说,“agent learns” 和 “learns agent” 这两个序列是完全等价的,因为它只关心词元之间的关系,而忽略了它们的排列。为了解决这个问题,Transformer 引入了位置编码 (Positional Encoding)  。

位置编码的核心思想是,为输入序列中的每一个词元嵌入向量,都额外加上一个能代表其绝对位置和相对位置信息的“位置向量”。这个位置向量不是通过学习得到的,而是通过一个固定的数学公式直接计算得出。这样一来,即使两个词元(例如,两个都叫 agent 的词元)自身的嵌入是相同的,但由于它们在句子中的位置不同,它们最终输入到 Transformer 模型中的向量就会因为加上了不同的位置编码而变得独一无二。原论文中提出的位置编码使用正弦和余弦函数来生成,其公式如下:

PE(pos,2i)=sin⁡(pos100002i/dmodel),PE(pos,2i)​=sin(100002i/dmodel​pos​),

PE(pos,2i+1)=cos⁡(pos100002i/dmodel)PE(pos,2i+1)​=cos(100002i/dmodel​pos​)

其中:

  • pospos 是词元在序列中的位置(例如,00,11,22,...)
  • ii 是位置向量中的维度索引(从 00 到 dmodel/2dmodel​/2)
  • dmodeldmodel​是词嵌入向量的维度(与我们模型中定义的一致)

现在,我们来实现 PositionalEncoding 模块,并完成我们 Transformer 骨架代码的最后一部分。

class PositionalEncoding(nn.Module):
    """
    为输入序列的词嵌入向量添加位置编码。
    """
    def __init__(self, d_model: int, dropout: float = 0.1, max_len: int = 5000):
        super().__init__()
        self.dropout = nn.Dropout(p=dropout)

        # 创建一个足够长的位置编码矩阵
        position = torch.arange(max_len).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model))

        # pe (positional encoding) 的大小为 (max_len, d_model)
        pe = torch.zeros(max_len, d_model)

        # 偶数维度使用 sin, 奇数维度使用 cos
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)

        # 将 pe 注册为 buffer,这样它就不会被视为模型参数,但会随模型移动(例如 to(device))
        self.register_buffer('pe', pe.unsqueeze(0))

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # x.size(1) 是当前输入的序列长度
        # 将位置编码加到输入向量上
        x = x + self.pe[:, :x.size(1)]
        return self.dropout(x)Copy to clipboardErrorCopied

本小节主要是为了帮助理解 Transformer 的宏观结构和内部每个模块的运作细节。由于是为了补充智能体学习中大模型的知识体系,也就不再继续往下深入实现。至此,我们已经为理解现代大语言模型打下了坚实的架构基础。在下一节中,我们将探讨 Decoder-Only 架构,看看它是如何基于 Transformer 的思想演变而来。

3.1.3 Decoder-Only 架构

前面一节中,我们动手构建了一个完整的Transformer 模型,它能在很多端到端的场景表现出色。但是当任务转换为构建一个与人对话、创作、作为智能体大脑的通用模型时,或许我们并不需要那么复杂的结构。

Transformer的设计哲学是“先理解,再生成”。编码器负责深入理解输入的整个句子,形成一个包含全局信息的上下文记忆,然后解码器基于这份记忆来生成翻译。但 OpenAI 在开发 GPT (Generative Pre-trained Transformer)  时,提出了一个更简单的思想[5]:语言的核心任务,不就是预测下一个最有可能出现的词吗?

无论是回答问题、写故事还是生成代码,本质上都是在一个已有的文本序列后面,一个词一个词地添加最合理的内容。基于这个思想,GPT 做了一个大胆的简化:它完全抛弃了编码器,只保留了解码器部分。  这就是 Decoder-Only 架构的由来。

Decoder-Only 架构的工作模式被称为自回归 (Autoregressive)  。这个听起来很专业的术语,其实描述了一个非常简单的过程:

  1. 给模型一个起始文本(例如 “Datawhale Agent is”)。
  2. 模型预测出下一个最有可能的词(例如 “a”)。
  3. 模型将自己刚刚生成的词 “a” 添加到输入文本的末尾,形成新的输入(“Datawhale Agent is a”)。
  4. 模型基于这个新输入,再次预测下一个词(例如 “powerful”)。
  5. 不断重复这个过程,直到生成完整的句子或达到停止条件。

模型就像一个在玩“文字接龙”的游戏,它不断地“回顾”自己已经写下的内容,然后思考下一个字该写什么。

你可能会问,解码器是如何保证在预测第 t 个词时,不去“偷看”第 t+1 个词的答案呢?

答案就是掩码自注意力 (Masked Self-Attention)  。在 Decoder-Only 架构中,这个机制变得至关重要。它的工作原理非常巧妙:

在自注意力机制计算出注意力分数矩阵(即每个词对其他所有词的关注度得分)之后,但在进行 Softmax 归一化之前,模型会应用一个“掩码”。这个掩码会将所有位于当前位置之后(即目前尚未观测到)的词元对应的分数,替换为一个非常大的负数。当这个带有负无穷分数的矩阵经过 Softmax 函数时,这些位置的概率就会变为 0。这样一来,模型在计算任何一个位置的输出时,都从数学上被阻止了去关注它后面的信息。这种机制保证了模型在预测下一个词时,能且仅能依赖它已经见过的、位于当前位置之前的所有信息,从而确保了预测的公平性和逻辑的连贯性。

Decoder-Only 架构的优势

这种看似简单的架构,却带来了巨大的成功,其优势在于:

  • 训练目标统一:模型的唯一任务就是“预测下一个词”,这个简单的目标非常适合在海量的无标注文本数据上进行预训练。
  • 结构简单,易于扩展:更少的组件意味着更容易进行规模化扩展。今天的 GPT-4、Llama 等拥有数千亿甚至万亿参数的巨型模型,都是基于这种简洁的架构。
  • 天然适合生成任务:其自回归的工作模式与所有生成式任务(对话、写作、代码生成等)完美契合,这也是它能成为构建通用智能体基础的核心原因。

总而言之,从 Transformer 的解码器演变而来的 Decoder-Only 架构,通过“预测下一个词”这一简单的范式,开启了我们今天所处的大语言模型时代。

3.2 与大语言模型交互

3.2.1 提示工程

如果我们把大语言模型比作一个能力极强的“大脑”,那么提示 (Prompt)  就是我们与这个“大脑”沟通的语言。提示工程,就是研究如何设计出精准的提示,从而引导模型产生我们期望输出的回复。对于构建智能体而言,一个精心设计的提示能让智能体之间协作分工变得高效。

(1)模型采样参数

在使用大模型时,你会经常看到类似Temperature这类的可配置参数,其本质是通过调整模型对 “概率分布” 的采样策略,让输出匹配具体场景需求,配置合适的参数可以提升Agent在特定场景的性能。

传统的概率分布是由 Softmax 公式计算得到的:pi=ezi∑j=1kezjpi​=∑j=1k​ezj​ezi​​,采样参数的本质就是在此基础上,根据不同策略“重新调整”或“截断”分布,从而改变大模型输出的下一个token。

Temperature:温度是控制模型输出 “随机性” 与 “确定性” 的关键参数。其原理是引入温度系数T>0T>0,将 Softmax 改写为pi(T)=ezi/T∑j=1kezj/Tpi(T)​=∑j=1k​ezj​/Tezi​/T​。

当T变小时,分布“更加陡峭”,高概率项权重进一步放大,生成更“保守”且重复率更高的文本。当T变大时,分布“更加平坦”,低概率项权重提升,生成更“多样”但可能出现不连贯的内容。

  • 低温度(0 ⩽⩽ Temperature << 0.3)时输出更 “精准、确定”。适用场景: 事实性任务:如问答、数据计算、代码生成; 严谨性场景:法律条文解读、技术文档撰写、学术概念解释等场景。
  • 中温度(0.3 ⩽⩽ Temperature << 0.7):输出 “平衡、自然”。适用场景: 日常对话:如客服交互、聊天机器人; 常规创作:如邮件撰写、产品文案、简单故事创作。
  • 高温度(0.7 ⩽⩽ Temperature << 2):输出 “创新、发散”。适用场景: 创意性任务:如诗歌创作、科幻故事构思、广告 slogan brainstorm、艺术灵感启发; 发散性思考。

Top-k :其原理是将所有 token 按概率从高到低排序,取排名前 k 个的 token 组成 “候选集”,随后对筛选出的 k 个 token 的概率进行 “归一化”: p^i=pi∑j∈候选集pjp^​i​=∑j∈候选集​pj​pi​​

  • 与温度采样的区别与联系:温度采样通过温度 T 调整所有 token 的概率分布(平滑或陡峭),不改变候选 token 的数量(仍考虑全部 N 个)。Top-k 采样通过 k 值限制候选 token 的数量(只保留前 k 个高概率 token),再从其中采样。当k=1时输出完全确定,退化为 “贪心采样”。

Top-p :其原理是将所有 token 按概率从高到低排序,从排序后的第一个 token 开始,逐步累加概率,直到累积和首次达到或超过阈值 p: ∑i∈Sp(i)≥p∑i∈S​p(i)​≥p,此时累加过程中包含的所有 token 组成 “核集合”,最后对核集合进行归一化。

  • 与Top-k的区别与联系:相对于固定截断大小的 Top-k,Top-p 能动态适应不同分布的“长尾”特性,对概率分布不均匀的极端情况的适应性更好。

在文本生成中,当同时设置 Top-p、Top-k 和温度系数时,这些参数会按照分层过滤的方式协同工作,其优先级顺序为:温度调整→Top-k→Top-p。温度调整整体分布的陡峭程度,Top-k 会先保留概率最高的 k 个候选,然后 Top-p 会从 Top-k 的结果中选取累积概率≥p 的最小集合作为最终的候选集。不过,通常 Top-k 和 Top-p 二选一即可,若同时设置,实际候选集为两者的交集。 需要注意的是,如果将温度设置为 0,则 Top-k 和 Top-p 将变得无关紧要,因为最有可能的 Token 将成为下一个预测的 Token;如果将 Top-k 设置为 1,温度和 Top-p 也将变得无关紧要,因为只有一个 Token 通过 Top-k 标准,它将是下一个预测的 Token。

(2)零样本、单样本与少样本提示

根据我们给模型提供示例(Exemplar)的数量,提示可以分为三种类型。为了更好地理解它们,让我们以一个情感分类任务为例,目标是让模型判断一段文本的情感色彩(如正面、负面或中性)。

零样本提示 (Zero-shot Prompting)  这指的是我们不给模型任何示例,直接让它根据指令完成任务。这得益于模型在海量数据上预训练后获得的强大泛化能力。

案例: 我们直接向模型下达指令,要求它完成情感分类任务。

文本:Datawhale的AI Agent课程非常棒!
情感:正面Copy to clipboardErrorCopied

单样本提示 (One-shot Prompting)  我们给模型提供一个完整的示例,向它展示任务的格式和期望的输出风格。

案例: 我们先给模型一个完整的“问题-答案”对作为示范,然后提出我们的新问题。

文本:这家餐厅的服务太慢了。
情感:负面

文本:Datawhale的AI Agent课程非常棒!
情感:Copy to clipboardErrorCopied

模型会模仿给出的示例格式,为第二段文本补全“正面”。

少样本提示 (Few-shot Prompting)  我们提供多个示例,这能让模型更准确地理解任务的细节、边界和细微差别,从而获得更好的性能。

案例: 我们提供涵盖了不同情况的多个示例,让模型对任务有更全面的理解。

文本:这家餐厅的服务太慢了。
情感:负面

文本:这部电影的情节很平淡。
情感:中性

文本:Datawhale的AI Agent课程非常棒!
情感:Copy to clipboardErrorCopied

模型会综合所有示例,更准确地将最后一句的情感分类为“正面”。

(3)指令调优的影响

早期的 GPT 模型(如 GPT-3)主要是“文本补全”模型,它们擅长根据前面的文本续写,但不一定能很好地理解并执行人类的指令。

指令调优 (Instruction Tuning)  是一种微调技术,它使用大量“指令-回答”格式的数据对预训练模型进行进一步的训练。经过指令调优后,模型能更好地理解并遵循用户的指令。我们今天日常工作学习中使用的所有模型(如 ChatGPTDeepSeekQwen)都是其模型家族中经过指令调优过的模型。

  • 对“文本补全”模型的提示(你需要用少样本提示“教会”模型做什么):
这是一段将英文翻译成中文的程序。
英文:Hello
中文:你好
英文:How are you?
中文:Copy to clipboardErrorCopied
  • 对“指令调优”模型的提示(你可以直接下达指令):
请将下面的英文翻译成中文:
How are you?Copy to clipboardErrorCopied

指令调优的出现,极大地简化了我们与模型交互的方式,使得直接、清晰的自然语言指令成为可能。

(4)基础提示技巧

角色扮演 (Role-playing)  通过赋予模型一个特定的角色,我们可以引导它的回答风格、语气和知识范围,使其输出更符合特定场景的需求。

# 案例
你现在是一位资深的Python编程专家。请解释一下Python中的GIL(全局解释器锁)是什么,要让一个初学者也能听懂。Copy to clipboardErrorCopied

上下文示例 (In-context Example)  这与少样本提示的思想一致,通过在提示中提供清晰的输入输出示例,来“教会”模型如何处理我们的请求,尤其是在处理复杂格式或特定风格的任务时非常有效。

# 案例
我需要你从产品评论中提取产品名称和用户情感。请严格按照下面的JSON格式输出。

评论:这款“星尘”笔记本电脑的屏幕显示效果惊人,但我不太喜欢它的键盘手感。
输出:{"product_name": "星尘笔记本电脑", "sentiment": "混合"}

评论:我刚买的“声动”耳机音质很棒,续航也超出了我的预期!
输出:Copy to clipboardErrorCopied

(5)思维链

对于需要逻辑推理、计算或多步骤思考的复杂问题,直接让模型给出答案往往容易出错。思维链 (Chain-of-Thought, CoT)  是一种强大的提示技巧,它通过引导模型“一步一步地思考”,提升了模型在复杂任务上的推理能力。

实现 CoT 的关键,是在提示中加入一句简单的引导语,如“请逐步思考”或“Let's think step by step”。

# 思维链提示
一个篮球队在一个赛季的80场比赛中赢了60%。在接下来的赛季中,他们打了15场比赛,赢了12场。两个赛季的总胜率是多少?
请一步一步地思考并解答。

>>>
(模型可能会输出)
好的,我们来一步步计算。
第一步:计算第一个赛季赢得的比赛数。
80场 * 60% = 48场。
第二步:计算两个赛季的总比赛数和总胜利数。
总比赛数 = 80 + 15 = 95场。
总胜利数 = 48 + 12 = 60场。
第三步:计算总胜率。
总胜率 = (总胜利数 / 总比赛数) * 100% = (60 / 95) * 100% ≈ 63.16%。
所以,两个赛季的总胜率约为63.16%。Copy to clipboardErrorCopied

通过显式地展示其推理过程,模型不仅更容易得出正确的答案,也让它的回答变得更可信、更易于我们检查和纠正。

3.2.2 文本分词

我们知道,计算机本质上只能理解数字。因此,在将自然语言文本喂给大语言模型之前,必须先将其转换成模型能够处理的数字格式。这个将文本序列转换为数字序列的过程,就叫做分词 (Tokenization)  。分词器 (Tokenizer)  的作用,就是定义一套规则,将原始文本切分成一个个最小的单元,我们称之为词元 (Token)  。

3.2.2.1 为何需要分词

早期的自然语言处理任务可能会采用简单的分词策略:

  • 按词分词 (Word-based)  :直接用空格或标点符号将句子切分成单词。这种方法很直观,但也面临挑战:

    • 词表爆炸与未登录词:一个语言的词汇量是巨大的,如果每个词都作为一个独立的词元,词表会变得难以管理。更糟糕的是,模型将无法处理任何未在词表中出现过的词(例如 “DatawhaleAgent”),这种现象我们称为“未登录词” (Out-Of-Vocabulary, OOV)。
    • 语义关联的缺失:模型难以捕捉词形相近的词之间的语义关系。例如,"look"、"looks" 和 "looking" 会被视为三个完全不同的词元,尽管它们有共同的核心含义。同样,训练数据中的低频词由于出现次数少,其语义也难以被模型充分学习。
  • 按字符分词 (Character-based)  :将文本切分成单个字符。这种方法词表很小(例如英文字母、数字和标点),不存在 OOV 问题。但它的缺点是,单个字符大多不具备独立的语义,模型需要花费更多的精力去学习如何将字符组合成有意义的词,导致学习效率低下。

为了兼顾词表大小和语义表达,现代大语言模型普遍采用子词分词 (Subword Tokenization)  算法。它的核心思想是:将常见的词(如 "agent")保留为完整的词元,同时将不常见的词(如 "Tokenization")拆分成多个有意义的子词片段(如 "Token" 和 "ization")。这样既控制了词表的大小,又能让模型通过组合子词来理解和生成新词。

3.2.2.2 字节对编码算法解析

字节对编码 (Byte-Pair Encoding, BPE) 是最主流的子词分词算法之一[6],GPT系列模型就采用了这种算法。其核心思想非常简洁,可以理解为一个“贪心”的合并过程:

  1. 初始化:将词表初始化为所有在语料库中出现过的基本字符。
  2. 迭代合并:在语料库上,统计所有相邻词元对的出现频率,找到频率最高的一对,将它们合并成一个新的词元,并加入词表。
  3. 重复:重复第 2 步,直到词表大小达到预设的阈值。

案例演示:  假设我们的迷你语料库是 {"hug": 1, "pug": 1, "pun": 1, "bun": 1},并且我们想构建一个大小为 10 的词表。BPE 的训练过程可以用下表3.1来表示:

表 3.1 BPE 算法合并过程示例

图片描述

训练结束后,词表大小达到 10,我们就得到了新的分词规则。现在,对于一个未见过的词 "bug",分词器会先查找 "bug" 是否在词表中,发现不在;然后查找 "bu",发现不在;最后查找 "b" 和 "ug",发现都在,于是将其切分为 ['b', 'ug']

下面我们用一段简单的 Python 代码来模拟上述过程:

import re, collections

def get_stats(vocab):
    """统计词元对频率"""
    pairs = collections.defaultdict(int)
    for word, freq in vocab.items():
        symbols = word.split()
        for i in range(len(symbols)-1):
            pairs[symbols[i],symbols[i+1]] += freq
    return pairs

def merge_vocab(pair, v_in):
    """合并词元对"""
    v_out = {}
    bigram = re.escape(' '.join(pair))
    p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
    for word in v_in:
        w_out = p.sub(''.join(pair), word)
        v_out[w_out] = v_in[word]
    return v_out

# 准备语料库,每个词末尾加上</w>表示结束,并切分好字符
vocab = {'h u g </w>': 1, 'p u g </w>': 1, 'p u n </w>': 1, 'b u n </w>': 1}
num_merges = 4 # 设置合并次数

for i in range(num_merges):
    pairs = get_stats(vocab)
    if not pairs:
        break
    best = max(pairs, key=pairs.get)
    vocab = merge_vocab(best, vocab)
    print(f"第{i+1}次合并: {best} -> {''.join(best)}")
    print(f"新词表(部分): {list(vocab.keys())}")
    print("-" * 20)

>>>
第1次合并: ('u', 'g') -> ug
新词表(部分): ['h ug </w>', 'p ug </w>', 'p u n </w>', 'b u n </w>']
--------------------
第2次合并: ('ug', '</w>') -> ug</w>
新词表(部分): ['h ug</w>', 'p ug</w>', 'p u n </w>', 'b u n </w>']
--------------------
第3次合并: ('u', 'n') -> un
新词表(部分): ['h ug</w>', 'p ug</w>', 'p un </w>', 'b un </w>']
--------------------
第4次合并: ('un', '</w>') -> un</w>
新词表(部分): ['h ug</w>', 'p ug</w>', 'p un</w>', 'b un</w>']
--------------------Copy to clipboardErrorCopied

这段代码清晰地展示了 BPE 算法如何通过迭代合并最高频的相邻词元对,来逐步构建和扩充词表的过程。

后续的许多算法都是在BPE的基础上进行优化的。其中,Google 开发的 WordPiece 和 SentencePiece 是影响力最大的两种。

  • WordPiece:Google BERT 模型采用的算法[7]。它与 BPE 非常相似,但合并词元的标准不是“最高频率”,而是“能最大化提升语料库的语言模型概率”。简单来说,它会优先合并那些能让整个语料库的“通顺度”提升最大的词元对。
  • SentencePiece:Google 开源的一款分词工具[8],Llama 系列模型采用了此算法。它最大的特点是,将空格也视作一个普通字符(通常用下划线 _ 表示)。这使得分词和解码过程完全可逆,且不依赖于特定的语言(例如,它不需要知道中文不使用空格分词)。

3.2.2.3 分词器对开发者的意义

理解分词算法的细节并非目的,但作为智能体的开发者,理解分词器的实际影响是重要,这直接关系到智能体的性能、成本和稳定性:

  • 上下文窗口限制:模型的上下文窗口(如 8K, 128K)是以 Token 数量计算的,而不是字符数或单词数。同样一段话,在不同语言(如中英文)或不同分词器下,Token 数量可能相差巨大。精确管理输入长度、避免超出上下文限制是构建长时记忆智能体的基础。
  • API 成本:大多数模型 API 都是按 Token 数量计费的。了解你的文本会被如何分词,是预估和控制智能体运行成本的关键一步。
  • 模型表现的异常:有时模型的奇怪表现根源在于分词。例如,模型可能很擅长计算 2 + 2,但对于 2+2(没有空格)就可能出错,因为后者可能被分词器视为一个独立的、不常见的词元。同样,一个词因为首字母大小写不同,也可能被切分成完全不同的 Token 序列,从而影响模型的理解。在设计提示词和解析模型输出时,考虑到这些“陷阱”有助于提升智能体的鲁棒性。

3.2.3 调用开源大语言模型

在本书的第一章,我们通过 API 来与大语言模型进行交互,以此驱动我们的智能体。这是一种快速、便捷的方式,但并非唯一的方式。对于许多需要处理敏感数据、希望离线运行或想精细控制成本的场景,将大语言模型直接部署在本地就显得至关重要。

Hugging Face Transformers 是一个强大的开源库,它提供了标准化的接口来加载和使用数以万计的预训练模型。我们将使用它来完成本次实践。

配置环境与选择模型:为了让大多数读者都能在个人电脑上顺利运行,我们特意选择了一个小规模但功能强大的模型:Qwen/Qwen1.5-0.5B-Chat。这是一个由阿里巴巴达摩院开源的拥有约 5 亿参数的对话模型,它体积小、性能优异,非常适合入门学习和本地部署。

首先,请确保你已经安装了必要的库:

pip install transformers torchCopy to clipboardErrorCopied

在 transformers 库中,我们通常使用 AutoModelForCausalLM 和 AutoTokenizer 这两个类来自动加载与模型匹配的权重和分词器。下面这段代码会自动从 Hugging Face Hub 下载所需的模型文件和分词器配置,这可能需要一些时间,具体取决于你的网络速度。

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

# 指定模型ID
model_id = "Qwen/Qwen1.5-0.5B-Chat"

# 设置设备,优先使用GPU
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using device: {device}")

# 加载分词器
tokenizer = AutoTokenizer.from_pretrained(model_id)

# 加载模型,并将其移动到指定设备
model = AutoModelForCausalLM.from_pretrained(model_id).to(device)

print("模型和分词器加载完成!")Copy to clipboardErrorCopied

我们来创建一个对话提示,Qwen1.5-Chat 模型遵循特定的对话模板。然后,可以将使用上一步加载的 tokenizer 将文本提示转换为模型能够理解的数字 ID(即 Token ID)。

# 准备对话输入
messages = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "你好,请介绍你自己。"}
]

# 使用分词器的模板格式化输入
text = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True
)

# 编码输入文本
model_inputs = tokenizer([text], return_tensors="pt").to(device)

print("编码后的输入文本:")
print(model_inputs)

>>>
{'input_ids': tensor([[151644, 8948, 198, 2610, 525, 264,  10950, 17847, 13,151645, 198, 151644, 872, 198, 108386, 37945, 100157, 107828,1773, 151645, 198, 151644, 77091, 198]], device='cuda:0'), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]],
       device='cuda:0')}Copy to clipboardErrorCopied

现在可以调用模型的 generate() 方法来生成回答了。模型会输出一系列 Token ID,这代表了它的回答。

最后,我们需要使用分词器的 decode() 方法,将这些数字 ID 翻译回人类可以阅读的文本。

# 使用模型生成回答
# max_new_tokens 控制了模型最多能生成多少个新的Token
generated_ids = model.generate(
    model_inputs.input_ids,
    max_new_tokens=512
)

# 将生成的 Token ID 截取掉输入部分
# 这样我们只解码模型新生成的部分
generated_ids = [
    output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]

# 解码生成的 Token ID
response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]

print("\n模型的回答:")
print(response)

>>>
我叫通义千问,是由阿里云研发的预训练语言模型,可以回答问题、创作文字,还能表达观点、撰写代码。我主要的功能是在多个领域提
供帮助,包括但不限于:语言理解、文本生成、机器翻译、问答系统等。有什么我可以帮到你的吗?Copy to clipboardErrorCopied

当你运行完所有代码后,你将会在本地电脑上看到模型生成的关于Qwen模型的介绍。恭喜你,你已经成功地在本地部署并运行了一个开源大语言模型!

3.2.4 模型的选择

在上一节中,我们成功地在本地运行了一个小型的开源语言模型。这自然引出了一个对于智能体开发者而言至关重要的问题:在当前数百个模型百花齐放的背景下,我们应当如何为特定的任务选择最合适的模型?

选择语言模型并非简单地追求“最大、最强”,而是一个在性能、成本、速度和部署方式之间进行权衡的决策过程。本节将首先梳理模型选型的几个关键考量因素,然后对当前主流的闭源与开源模型进行梳理。

由于大语言模型技术正处于高速发展阶段,新模型、新版本层出不穷,迭代速度极快。本节在撰写时力求提供当前主流模型的概览和选型考量,但请读者注意,文中所提及的具体模型版本和性能数据可能随时间推移而发生变化,且只列举了部分工作并不完整。我们更侧重于介绍其核心技术特点、发展趋势以及在智能体开发中的通用选型原则。

3.2.4.1 模型选型的关键考量

在为您的智能体选择大语言模型时,可以从以下几个维度进行综合评估:

  • 性能与能力:这是最核心的考量。不同的模型擅长的任务不同,有的长于逻辑推理和代码生成,有的则在创意写作或多语言翻译上更胜一筹。您可以参考一些公开的基准测试排行榜(如 LMSys Chatbot Arena Leaderboard)来评估模型的综合能力。
  • 成本:对于闭源模型,成本主要体现在 API 调用费用,通常按 Token 数量计费。对于开源模型,成本则体现在本地部署所需的硬件(GPU、内存)和运维上。需要根据应用的预期使用量和预算做出选择。
  • 速度(延迟) :对于需要实时交互的智能体(如客服、游戏 NPC),模型的响应速度至关重要。一些轻量级或经过优化的模型(如 GPT-3.5 Turbo, Claude 3.5 Sonnet)在延迟上表现更优。
  • 上下文窗口:模型能一次性处理的 Token 数量上限。对于需要理解长文档、分析代码库或维持长期对话记忆的智能体,选择一个拥有较大上下文窗口(如 128K Token 或更高)的模型是必要的。
  • 部署方式:使用 API 的方式最简单便捷,但数据需要发送给第三方,且受限于服务商的条款。本地部署则能确保数据隐私和最高程度的自主可控,但对技术和硬件要求更高。
  • 生态与工具链:一个模型的流行程度也决定了其周边生态的成熟度。主流模型通常拥有更丰富的社区支持、教程、预训练模型、微调工具和兼容的开发框架(如 LangChain, LlamaIndex, Hugging Face Transformers),这能极大地加速开发进程,降低开发难度。选择一个拥有活跃社区和完善工具链的模型,可以在遇到问题时更容易找到解决方案和资源。
  • 可微调性与定制化:对于需要处理特定领域数据或执行特定任务的智能体,模型的微调能力至关重要。一些模型提供了便捷的微调接口和工具,允许开发者使用自己的数据集对模型进行定制化训练,从而显著提升模型在特定场景下的性能和准确性。开源模型在这方面通常提供更大的灵活性。
  • 安全性与伦理:随着大语言模型的广泛应用,其潜在的安全风险和伦理问题也日益凸显。选择模型时,需要考虑其在偏见、毒性、幻觉等方面的表现,以及服务商或开源社区在模型安全和负责任AI方面的投入。对于面向公众或涉及敏感信息的应用,模型的安全性和伦理合规性是不可忽视的考量。

3.2.4.2 闭源模型概览

闭源模型通常代表了当前 AI 技术的最前沿,并提供稳定、易用的 API 服务,是构建高性能智能体的首选。

  1. OpenAI GPT 系列:从开启大模型时代的 GPT-3,到引入 RLHF(人类反馈强化学习)、实现与人类意图对齐的 ChatGPT,再到开启多模态时代的 GPT-4,OpenAI 持续引领行业发展。最新的 GPT-5 更是将多模态能力和通用智能水平提升到新的高度,能够无缝处理文本、音频和图像输入,并生成相应的输出,其响应速度和自然度也大幅提升,尤其在实时语音对话方面表现出色。
  2. Google Gemini 系列:Google DeepMind 推出的 Gemini 系列模型是原生多模态的代表,其核心特点是能统一处理文本、代码、音视频和图像等多种模态的数据,并以其超长的上下文窗口在海量信息处理上具备优势。Gemini Ultra 是其最强大的模型,适用于高度复杂的任务;Gemini Pro 适用于广泛的任务,提供高性能和效率;Gemini Nano 则针对设备端部署进行了优化。最新的 Gemini 2.5 系列模型,如 Gemini 2.5 Pro 和 Gemini 2.5 Flash,进一步提升了推理能力和上下文窗口,特别是 Gemini 2.5 Flash 以其更快的推理速度和成本效益,适用于需要快速响应的场景。
  3. Anthropic Claude 系列:Anthropic 是一家专注于 AI 安全和负责任 AI 的公司,其 Claude 系列模型从设计之初就将 AI 安全放在首位,以其在处理长文档、减少有害输出、遵循指令方面的可靠性而闻名,深受企业级应用青睐。Claude 3 系列包括 Claude 3 Opus(最智能、性能最强)、Claude 3 Sonnet(性能与速度兼顾的平衡之选)和 Claude 3 Haiku(最快、最紧凑的模型,适用于近乎实时的交互)。最新的 Claude 4 系列模型,如 Claude 4 Opus,在通用智能、复杂推理和代码生成方面取得了显著进展,进一步提升了处理长上下文和多模态任务的能力。
  4. 国内主流模型:中国在大语言模型领域涌现出众多具有竞争力的闭源模型,以百度文心一言(ERNIE Bot)、腾讯混元(Hunyuan)、华为盘古(Pangu-α)、科大讯飞星火(SparkDesk)和月之暗面(Moonshot AI)等为代表的国产模型,在中文处理上具备天然优势,并深度赋能本土产业。

3.2.4.3 开源模型概览

开源模型为开发者提供了最高程度的灵活性、透明度和自主性,催生了繁荣的社区生态。它们允许开发者在本地部署、进行定制化微调,并拥有完整的模型控制权。

  • Meta Llama 系列:Meta 推出的 Llama 系列是开源大语言模型的重要里程碑。该系列凭借出色的综合性能、开放的许可协议和强大的社区支持,成为许多衍生项目和研究的基座。Llama 4 系列于2025年4月发布,是Meta首批采用混合专家(MoE)架构的模型,该架构通过仅激活处理特定任务所需的模型部分来显著提升计算效率。该系列包含三款定位分明的模型:LLama 4 Scout支持1000万token的上下文窗口专为长文档分析和移动端部署设计。Llama 4 Maverick专注于多模态能力,在编码、复杂推理及多语言支持方面表现卓越。Llama 4 Behemoth多项STEM基准测试中表现超越竞争对手。是Meta目前最强大的模型
  • Mistral AI 系列:来自法国的 Mistral AI 以其“小尺寸、高性能”的模型设计而闻名。其最新模型 Mistral Medium 3.1 于2025年8月发布,在代码生成、STEM推理和跨领域问答等任务上准确率与响应速度均有显著提升,基准测试表现优于Claude Sonnet 3.7与Llama 4 Maverick等同级模型。它具备原生多模态能力,可同时处理图像与文字混合输入,并内置“语调适配层”,帮助企业更轻松实现符合品牌调性的输出。
  • 国内开源力量:国内厂商和科研机构也在积极拥抱开源,例如阿里巴巴的通义千问 (Qwen)  系列和清华大学与智谱 AI 合作的 ChatGLM 系列,它们提供了强大的中文能力,并围绕自身构建了活跃的社区。

对于智能体开发者而言,闭源模型提供了“开箱即用”的便捷,而开源模型则赋予了我们“随心所欲”的定制自由。理解这两大阵营的特点和代表模型,是为我们的智能体项目做出明智技术选型的第一步。

3.3 大语言模型的缩放法则与局限性

大语言模型(LLMs)在近年来取得了令人瞩目的进展,其能力边界不断拓展,应用场景日益丰富。然而,这些成就的背后,离不开对模型规模、数据量和计算资源之间关系的深刻理解,即缩放法则(Scaling Laws) 。同时,作为新兴技术,LLMs也面临着诸多挑战和局限性。本节将深入探讨这些核心概念,旨在帮助读者全面理解LLMs的能力边界,从而在构建智能体时扬长避短。

3.3.1 缩放法则

缩放法则(Scaling Laws) 是近年来大语言模型领域最重要的发现之一。它揭示了模型性能与模型参数量、训练数据量以及计算资源之间存在着可预测的幂律关系。这一发现为大语言模型的持续发展提供了理论指导,阐明了增加资源投入能够系统性提升模型性能的底层逻辑。

研究发现,在对数-对数坐标系下,模型的性能(通常用损失 Loss 来衡量)与参数量、数据量和计算量这三个因素都呈现出平滑的幂律关系[9]。简单来说,只要我们持续、按比例地增加这三个要素,模型的性能就会可预测地、平滑地提升,而不会出现明显的瓶颈。这一发现为大模型的设计和训练提供了清晰的指导:在资源允许的范围内,尽可能地扩大模型规模和训练数据量。

早期的研究更侧重于增加模型参数量,但 DeepMind 在 2022 年提出的“Chinchilla 定律”对此进行了重要修正[10]。该定律指出,在给定的计算预算下,为了达到最优性能,模型参数量和训练数据量之间存在一个最优配比。具体来说,最优的模型应该比之前普遍认为的要小,但需要用多得多的数据进行训练。例如,一个 700 亿参数的 Chinchilla 模型,由于使用了比 GPT-3(1750 亿参数)多 4 倍的数据进行训练,其性能反而超越了后者。这一发现纠正了“越大越好”的片面认知,强调了数据效率的重要性,并指导了后续许多高效大模型(如 Llama 系列)的设计。

缩放法则最令人惊奇的产物是“能力的涌现”。所谓能力涌现,是指当模型规模达到一定阈值后,会突然展现出在小规模模型中完全不存在或表现不佳的全新能力。例如,链式思考 (Chain-of-Thought)  、指令遵循 (Instruction Following)  、多步推理、代码生成等能力,都是在模型参数量达到数百亿甚至千亿级别后才显著出现的。这种现象表明,大语言模型不仅仅是简单地记忆和复述,它们在学习过程中可能形成了某种更深层次的抽象和推理能力。对于智能体开发者而言,能力的涌现意味着选择一个足够大规模的模型,是实现复杂自主决策和规划能力的前提。

3.3.2 模型幻觉

模型幻觉(Hallucination) 通常指的是大语言模型生成的内容与客观事实、用户输入或上下文信息相矛盾,或者生成了不存在的事实、实体或事件。幻觉的本质是模型在生成过程中,过度自信地“编造”了信息,而非准确地检索或推理。根据其表现形式,幻觉可以被分为多种类型[11],例如:

  • 事实性幻觉 (Factual Hallucinations)  : 模型生成与现实世界事实不符的信息。
  • 忠实性幻觉 (Faithfulness Hallucinations)  : 在文本摘要、翻译等任务中,生成的内容未能忠实地反映源文本的含义。
  • 内在幻觉 (Intrinsic Hallucinations)  : 模型生成的内容与输入信息直接矛盾。

幻觉的产生是多方面因素共同作用的结果。首先,训练数据中可能包含错误或矛盾的信息。其次,模型的自回归生成机制决定了它只是在预测下一个最可能的词元,而没有内置的事实核查模块。最后,在面对需要复杂推理的任务时,模型可能会在逻辑链条中出错,从而“编造”出错误的结论。例如:一个旅游规划 Agent,可能会为你推荐一个现实中不存在的景点,或者预订一个航班号错误的机票。

此外,大语言模型还面临着知识时效性不足和训练数据中存在的偏见等挑战。大语言模型的能力来源于其训练数据。这意味着模型所掌握的知识是其训练数据收集时的最新材料。对于在此日期之后发生的事件、新出现的概念或最新的事实,模型将无法感知或正确回答。与此同时训练数据往往包含了人类社会的各种偏见和刻板印象。当模型在这些数据上学习时,它不可避免地会吸收并反映出这些偏见[12]。

为了提高大语言模型的可靠性,研究人员和开发者正在积极探索多种检测和缓解幻觉的方法:

  1. 数据层面: 通过高质量数据清洗、引入事实性知识以及强化学习与人类反馈 (RLHF) 等方式[13],从源头减少幻觉。

  2. 模型层面: 探索新的模型架构,或让模型能够表达其对生成内容的不确定性。

  3. 推理与生成层面

    1. 检索增强生成 (Retrieval-Augmented Generation, RAG)  [14]: 这是目前缓解幻觉的有效方法之一。RAG 系统通过在生成之前从外部知识库(如文档数据库、网页)中检索相关信息,然后将检索到的信息作为上下文,引导模型生成基于事实的回答。
    2. 多步推理与验证: 引导模型进行多步推理,并在每一步进行自我检查或外部验证。
    3. 引入外部工具: 允许模型调用外部工具(如搜索引擎、计算器、代码解释器)来获取实时信息或进行精确计算。

尽管幻觉问题短期内难以完全消除,但通过上述的策略,可以显著降低其发生频率和影响,提高大语言模型在实际应用中的可靠性和实用性。

3.4 本章小结

本章介绍了构建智能体所需的基础知识,重点围绕作为其核心组件的大语言模型 (LLM) 展开。内容从语言模型的早期发展开始,详细讲解了 Transformer 架构,并介绍了与 LLM 进行交互的方法。最后,本章对当前主流的模型生态、发展规律及其固有局限性进行了梳理。

核心知识点回顾:

  • 模型演进与核心架构:本章追溯了从统计语言模型 (N-gram) 到神经网络模型 (RNN, LSTM),再到奠定现代 LLM 基础的 Transformer 架构。通过“自顶向下”的代码实现,本章拆解了 Transformer 的核心组件,并阐述了自注意力机制在并行计算和捕捉长距离依赖中的关键作用。
  • 与模型的交互方式:本章介绍了与 LLM 交互的两个核心环节:提示工程 (Prompt Engineering) 和文本分词 (Tokenization)。前者用于指导模型的行为,后者是理解模型输入处理的基础。通过本地部署并运行开源模型的实践,将理论知识应用于实际操作。
  • 模型生态与选型:本章系统地梳理了为智能体选择模型时需要权衡的关键因素,并概览了以 OpenAI GPT、Google Gemini 为代表的闭源模型和以 Llama、Mistral 为代表的开源模型的特点与定位。
  • 法则与局限:本章探讨了驱动 LLM 能力提升的缩放法则,阐述了其背后的基本原理。同时,本章也分析了模型存在的如事实幻觉、知识过时等固有局限性,这对于构建可靠、鲁棒的智能体至关重要。

从 LLM 基础到构建智能体:

这一章的LLM基础主要是为了帮助大家更好的理解大模型的诞生以及发展过程,其中也蕴含了智能体设计的部分思考。例如,如何设计有效的提示词来引导 Agent 的规划与决策,如何根据任务需求选择合适的模型,以及如何在 Agent 的工作流中加入验证机制以规避模型的幻觉等问题,其解决方案均建立在本章的基础之上。我们现在已经准备好从理论转向实践。在下一章,我们将开始探索智能体经典范式构建,将本章所学的知识应用于实际的智能体设计之中。