HELLO-AGENTS

https://github.com/datawhalechina/hello-agents

task00

task00:前言

  • 了解了学习目标和内容。
  • 大体的五部分划分和学习方向。
pip install "camel-ai[all]==0.2.38"

pip install "camel-ai[all]==0.2.38"
#%%
from camel.agents import ChatAgent
from camel.models import ModelFactory
from camel.types import ModelPlatformType

model = ModelFactory.create(
    model_platform=ModelPlatformType.OPENAI_COMPATIBLE_MODEL,
    model_type="Qwen/Qwen2.5-72B-Instruct",
    url='https://api-inference.modelscope.cn/v1/',
    api_key='ms-xxxx你的魔搭API-KEY'
)

agent = ChatAgent(
    model=model,
    output_language='中文'
)

response = agent.step("你好,你是谁?")
print(response.msgs[0].content)

# >>>
# 你好,我是来自阿里云的大规模语言模型,我叫通义千问。

image

task01

task01:

  • 了解了什么是智能体。
  • 大语言模型的范式。
  • 智能体的类型。
  • 了解了 Workflow 和 Agent 的区别。

Agent的工作过程

  1. 感知 (Perception);
  2. 思考 (Thought);
    2.1. 规划 (Planning);
    2.2. 行动/执行 (Action);
  3. 智能体的行动会引起环境 (Environment) 的状态变化 (State Change),环境随即会产生一个新的观察 (Observation) 作为结果反馈。
  4. 形成一个持续的“感知-思考-行动-观察”的闭环
  • 感知环境
  • 规划行动
  • 执行行动
  • 评估结果
  • 迭代优化
    智能体的核心是 Thought-Action-Observation(TAO)循环.

PEAS

  • 性能度量(Performance)
  • 环境(Environment)
  • 执行器(Actuators)
  • 传感器(Sensors)

image
然而,其“阿喀琉斯之踵”在于脆弱性:它依赖于一个完备的规则体系,但在充满模糊和例外的现实世界中,任何未被覆盖的新情况都可能导致系统失灵,这就是所谓的“知识获取瓶颈”。
image
image
image

5 分钟实现第一个智能体

# (1)指令模板
TAVILY_API_KEY = 'tvly-dev-XXXX 你申请的KEY' # 地址:https://www.tavily.com/
YOUR_API_KEY = 'ms-xxxxx 你的魔搭API-KEY'

AGENT_SYSTEM_PROMPT = """
你是一个智能旅行助手。你的任务是分析用户的请求,并使用可用工具一步步地解决问题。

# 可用工具:
- `get_weather(city: str)`: 查询指定城市的实时天气。
- `get_attraction(city: str, weather: str)`: 根据城市和天气搜索推荐的旅游景点。

# 行动格式:
你的回答必须严格遵循以下格式。首先是你的思考过程,然后是你要执行的具体行动,每次回复只输出一对Thought-Action:
Thought: [这里是你的思考过程和下一步计划]
Action: [这里是你要调用的工具,格式为 function_name(arg_name="arg_value")]

# 任务完成:
当你收集到足够的信息,能够回答用户的最终问题时,你必须在`Action:`字段后使用 `finish(answer="...")` 来输出最终答案。

请开始吧!
"""

# (2)工具 1:查询真实天气

import requests
import json


def get_weather(city: str) -> str:
  """
  通过调用 wttr.in API 查询真实的天气信息。
  """
  # API端点,我们请求JSON格式的数据
  url = f"https://wttr.in/{city}?format=j1"

  try:
    # 发起网络请求
    response = requests.get(url)
    # 检查响应状态码是否为200 (成功)
    response.raise_for_status()
    # 解析返回的JSON数据
    data = response.json()

    # 提取当前天气状况
    current_condition = data['current_condition'][0]
    weather_desc = current_condition['weatherDesc'][0]['value']
    temp_c = current_condition['temp_C']

    # 格式化成自然语言返回
    return f"{city}当前天气:{weather_desc},气温{temp_c}摄氏度"

  except requests.exceptions.RequestException as e:
    # 处理网络错误
    return f"错误:查询天气时遇到网络问题 - {e}"
  except (KeyError, IndexError) as e:
    # 处理数据解析错误
    return f"错误:解析天气数据失败,可能是城市名称无效 - {e}"


# (3)工具 2:搜索并推荐旅游景点

import os
from tavily import TavilyClient


def get_attraction(city: str, weather: str) -> str:
  """
  根据城市和天气,使用Tavily Search API搜索并返回优化后的景点推荐。
  """
  # 1. 从环境变量中读取API密钥
  api_key = TAVILY_API_KEY  # os.environ.get("TAVILY_API_KEY")
  if not api_key:
    return "错误:未配置TAVILY_API_KEY环境变量。"

  # 2. 初始化Tavily客户端
  tavily = TavilyClient(api_key=api_key)

  # 3. 构造一个精确的查询
  query = f"'{city}' 在'{weather}'天气下最值得去的旅游景点推荐及理由"

  try:
    # 4. 调用API,include_answer=True会返回一个综合性的回答
    response = tavily.search(query=query, search_depth="basic", include_answer=True)

    # 5. Tavily返回的结果已经非常干净,可以直接使用
    # response['answer'] 是一个基于所有搜索结果的总结性回答
    if response.get("answer"):
      return response["answer"]

    # 如果没有综合性回答,则格式化原始结果
    formatted_results = []
    for result in response.get("results", []):
      formatted_results.append(f"- {result['title']}: {result['content']}")

    if not formatted_results:
      return "抱歉,没有找到相关的旅游景点推荐。"

    return "根据搜索,为您找到以下信息:\n" + "\n".join(formatted_results)

  except Exception as e:
    return f"错误:执行Tavily搜索时出现问题 - {e}"


# 将所有工具函数放入一个字典,方便后续调用
available_tools = {
  "get_weather"   : get_weather,
  "get_attraction": get_attraction,
}

# 1.3.2 接入大语言模型

from openai import OpenAI


class OpenAICompatibleClient:
  """
  一个用于调用任何兼容OpenAI接口的LLM服务的客户端。
  """

  def __init__(self, model: str, api_key: str, base_url: str):
    self.model = model
    self.client = OpenAI(api_key=api_key, base_url=base_url)

  def generate(self, prompt: str, system_prompt: str) -> str:
    """调用LLM API来生成回应。"""
    print("正在调用大语言模型...")
    try:
      messages = [
        {'role': 'system', 'content': system_prompt},
        {'role': 'user', 'content': prompt}
      ]
      response = self.client.chat.completions.create(
          model=self.model,
          messages=messages,
          stream=False
      )
      answer = response.choices[0].message.content
      print("大语言模型响应成功。")
      return answer
    except Exception as e:
      print(f"调用LLM API时发生错误: {e}")
      return "错误:调用语言模型服务时出错。"


# 1.3.3 执行行动循环

import re

# --- 1. 配置LLM客户端 ---
# 请根据您使用的服务,将这里替换成对应的凭证和地址
API_KEY = YOUR_API_KEY  #os.environ.get("YOUR_API_KEY")  # YOUR_API_KEY"
BASE_URL = 'https://api-inference.modelscope.cn/v1/'  #os.environ.get("YOUR_BASE_URL")  # "YOUR_BASE_URL" 参考 https://modelscope.cn/my/myaccesstoken
MODEL_ID = 'Qwen/Qwen2.5-Coder-32B-Instruct'  #os.environ.get("YOUR_MODEL_ID")  # "YOUR_MODEL_ID"
# TAVILY_API_KEY = TAVILY_API_KEY# os.environ.get("TAVILY_API_KEY")  # "YOUR_Tavily_KEY"
os.environ['TAVILY_API_KEY'] = TAVILY_API_KEY  #"YOUR_TAVILY_API_KEY"

llm = OpenAICompatibleClient(
    model=MODEL_ID,
    api_key=API_KEY,
    base_url=BASE_URL
)

# --- 2. 初始化 ---
# user_prompt = "你好,请帮我查询一下今天北京的天气,然后根据天气推荐一个合适的旅游景点。"
user_prompt = "你好,请帮我查询一下今天上海的天气,然后根据天气推荐一个合适的旅游景点。"
prompt_history = [f"用户请求: {user_prompt}"]

print(f"用户输入: {user_prompt}\n" + "=" * 40)

# --- 3. 运行主循环 ---
for i in range(5):  # 设置最大循环次数
  print(f"--- 循环 {i + 1} ---\n")

  # 3.1. 构建Prompt
  full_prompt = "\n".join(prompt_history)

  # 3.2. 调用LLM进行思考
  llm_output = llm.generate(full_prompt, system_prompt=AGENT_SYSTEM_PROMPT)
  # 模型可能会输出多余的Thought-Action,需要截断
  match = re.search(r'(Thought:.*?Action:.*?)(?=\n\s*(?:Thought:|Action:|Observation:)|\Z)', llm_output, re.DOTALL)
  if match:
    truncated = match.group(1).strip()
    if truncated != llm_output.strip():
      llm_output = truncated
      print("已截断多余的 Thought-Action 对")
  print(f"模型输出:\n{llm_output}\n")
  prompt_history.append(llm_output)

  # 3.3. 解析并执行行动
  action_match = re.search(r"Action: (.*)", llm_output, re.DOTALL)
  if not action_match:
    print("解析错误:模型输出中未找到 Action。")
    break
  action_str = action_match.group(1).strip()

  if action_str.startswith("finish"):
    final_answer = re.search(r'finish\(answer="(.*)"\)', action_str).group(1)
    print(f"任务完成,最终答案: {final_answer}")
    break

  tool_name = re.search(r"(\w+)\(", action_str).group(1)
  args_str = re.search(r"\((.*)\)", action_str).group(1)
  kwargs = dict(re.findall(r'(\w+)="([^"]*)"', args_str))

  if tool_name in available_tools:
    observation = available_tools[tool_name](**kwargs)
  else:
    observation = f"错误:未定义的工具 '{tool_name}'"

  # 3.4. 记录观察结果
  observation_str = f"Observation: {observation}"
  print(f"{observation_str}\n" + "=" * 40)
  prompt_history.append(observation_str)

image

习题四

习题六

  1. 智能体 “幻觉” 的产生原因
    “幻觉” 是智能体生成 “看似合理但实际错误” 信息的现象,核心原因的是 “统计匹配≠事实理解”:
    模型本质缺陷:大语言模型(LLM)是基于训练数据的 “下一个词预测”,而非真正理解事实。它会学习数据中的统计规律(如 “某景点门票价格” 常与 “XX 元” 关联),但无法判断信息的真实性;
    训练数据问题:训练数据中可能存在错误、矛盾、过时的信息(如旧的景点开放时间),模型学到后会生成错误内容;
    缺乏实时核查:智能体未接入外部真实数据源(如实时门票系统、最新政策文档),仅依赖内部知识,导致信息滞后或错误;
    上下文遗忘:长文本处理时,上下文窗口有限,可能遗忘关键信息(如用户说 “预算 3000”,后续推荐时生成 “5000 元高端酒店”);
    迎合用户倾向:模型为了满足用户需求,会生成 “看似合理” 的内容(如用户问 “某景点是否开放”,模型不确定时仍会给出 “开放时间为 XX” 的答案)。
  2. 无最大循环次数限制的问题
    若智能体的 TAO 循环无次数限制,可能陷入以下困境:
    无限循环:如 “推荐景点→用户拒绝→调整推荐→用户再拒绝→再调整”,一直循环无法终止(尤其是用户需求不明确时);
    资源耗尽:每次循环需消耗 CPU、内存(如查询 API、推理计算),无限循环会导致系统崩溃;
    任务漂移:多次调整后偏离初始目标(如用户要 “历史景点”,调整后变成推荐 “美食”);
    用户体验恶化:持续推送无效推荐,让用户厌烦(如连续 10 次推荐用户不喜欢的景点);
    逻辑死锁:如备选景点一直售罄,智能体反复查询无结果,无法停止。

task02

image

import re
import random

# 定义规则库:模式(正则表达式) -> 响应模板列表
rules = {
    r'I need (.*)': [
        "Why do you need {0}?",
        "Would it really help you to get {0}?",
        "Are you sure you need {0}?"
    ],
    r'Why don\'t you (.*)\?': [
        "Do you really think I don't {0}?",
        "Perhaps eventually I will {0}.",
        "Do you really want me to {0}?"
    ],
    r'Why can\'t I (.*)\?': [
        "Do you think you should be able to {0}?",
        "If you could {0}, what would you do?",
        "I don't know -- why can't you {0}?"
    ],
    r'I am (.*)': [
        "Did you come to me because you are {0}?",
        "How long have you been {0}?",
        "How do you feel about being {0}?"
    ],
    r'.* mother .*': [
        "Tell me more about your mother.",
        "What was your relationship with your mother like?",
        "How do you feel about your mother?"
    ],
    r'.* father .*': [
        "Tell me more about your father.",
        "How did your father make you feel?",
        "What has your father taught you?"
    ],
    r'.*': [
        "Please tell me more.",
        "Let's change focus a bit... Tell me about your family.",
        "Can you elaborate on that?"
    ]
}

# 定义代词转换规则
pronoun_swap = {
    "i": "you", "you": "i", "me": "you", "my": "your",
    "am": "are", "are": "am", "was": "were", "i'd": "you would",
    "i've": "you have", "i'll": "you will", "yours": "mine",
    "mine": "yours"
}

def swap_pronouns(phrase):
    """
    对输入短语中的代词进行第一/第二人称转换
    """
    words = phrase.lower().split()
    swapped_words = [pronoun_swap.get(word, word) for word in words]
    return " ".join(swapped_words)

def respond(user_input):
    """
    根据规则库生成响应
    """
    for pattern, responses in rules.items():
        match = re.search(pattern, user_input, re.IGNORECASE)
        if match:
            # 捕获匹配到的部分
            captured_group = match.group(1) if match.groups() else ''
            # 进行代词转换
            swapped_group = swap_pronouns(captured_group)
            # 从模板中随机选择一个并格式化
            response = random.choice(responses).format(swapped_group)
            return response
    # 如果没有匹配任何特定规则,使用最后的通配符规则
    return random.choice(rules[r'.*'])

# 主聊天循环
if __name__ == '__main__':
    print("Therapist: Hello! How can I help you today?")
    while True:
        user_input = input("You: ")
        if user_input.lower() in ["quit", "exit", "bye"]:
            print("Therapist: Goodbye. It was nice talking to you.")
            break
        response = respond(user_input)
        print(f"Therapist: {response}")
        
# >>>
# Therapist: Hello! How can I help you today?
# You: I am feeling sad today.
# Therapist: How long have you been feeling sad today?
# You: I need some help with my project.
# Therapist: Are you sure you need some help with your project?
# You: My mother is not happy with my work.
# Therapist: Tell me more about your mother.
# You: quit
# Therapist: Goodbye. It was nice talking to you.

image
task02:

  • 充分 + 必要
  • 符号 -> 专家系统 -> 模式匹配 -> 【元年】协作体 (颠覆性构想,不再将心智视为一个金字塔式的层级结构,而是将其看作一个扁平化的、充满了互动与协作的“社会”。){智能本身就是“不完美”的、由许多功能各异、甚至会彼此冲突的简单部分组成的大杂烩。} -> 机构(Agency) /
    涌现(Emergence) -> 多智能体 -> 联结主义(Connectionism)/ 强化学习(Reinforcement Learning, RL) -> 行为主义 (Behaviorism)
    image
    image
    image
    image
    image

task03

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.167

image
task03:

  • 现代智能体是如何工作的?
    -- 概率
    -- 完整的链式法则 -> 马尔可夫假设 (Markov Assumption) -> N-gram模型
    image
    -- 我们不必回溯一个词的全部历史,可以近似地认为,一个词的出现概率只与它前面有限的 n−1 个词有关
    --- Bigram :N=2 最简单的情况,假设一个词的出现只与它前面的一个词有关。
    --- Trigram: N=3 假设一个词的出现只与它前面的两个词有关。
    --- 最大似然估计(Maximum Likelihood Estimation,MLE) 最可能出现的,就是我们在数据中看到次数最多的。
    -- 致命缺陷
    --- 稀疏性问题:当语料库中某些词的出现次数非常少时,它们的概率估计变得不准。
    --- 泛化能力差:本质是不理解。
  • 前馈神经网络语言模型 (Feedforward Neural Network Language Model)
    image
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.0000

image
-- 语义空间 ,词嵌入 (Word Embedding) 或词向量 ,高维的连续向量空间,词汇表中的每个词都映射为该空间中的一个点。语义上相近的词,它们对应的向量在空间中的位置也相近。
-- 从上下文到下一个词的映射 ,函数的输入是前 n−1 个词的词向量,输出是词汇表中每个词在当前上下文后出现的概率分布。
余弦相似度 (Cosine Similarity) ,它通过计算两个向量夹角的余弦值来衡量它们的相似性。

  • 循环神经网络 (RNN) 与长短时记忆网络 (LSTM) -> 循环神经网络 (Recurrent Neural Network, RNN) 为网络增加“记忆”能力, 引入了一个隐藏状态 (hidden state) 向量(网络的短期记忆)
    image
  • 循环神经网络 (RNN) 与长短时记忆网络 (LSTM) -> 循环神经网络 (Recurrent Neural Network, RNN) 为网络增加“记忆”能力, 引入了一个隐藏状态 (hidden state) 向量(网络的短期记忆)
    -- 缺陷
    --- 长期依赖问题 (Long-term Dependency Problem)
    -> 长短时记忆网络 (Long Short-Term Memory, LSTM) 解决,引入细胞状态 (Cell State) 和 门控机制 (Gating Mechanism)
    • 细胞状态 (Cell State) :用于存储长期信息,独立于隐藏状态的信息通路。
    • 门控机制 (Gating Mechanism) :包括遗忘门、输入门和输出门,用于控制信息在细胞状态中的流动。
      -- 遗忘门 (Forget Gate):决定从上一时刻的细胞状态中丢弃哪些信息。
      -- 输入门 (Input Gate):决定将当前输入中的哪些新信息存入细胞状态。
      -- 输出门 (Output Gate):决定根据当前的细胞状态,输出哪些信息到隐藏状态。

-> Transformer 架构 ,提高大规模的并行计算能力,完全抛弃了循环结构,转而完全依赖一种名为注意力 (Attention) 的机制来捕捉序列内的依赖关系。

1.Encoder-Decoder 整体结构
2.从自注意力到多头注意力
3.前馈神经网络
4.残差连接与层归一化

Encoder-Decoder 整体结构,为端到端任务机器翻译而设计。

image
-- 编码器 (Encoder) :任务是“理解”输入的整个句子。
-- 解码器 (Decoder) :任务是“生成”目标句子。

从自注意力到多头注意力

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

引入了三个可学习的角色:

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

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

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

import torch
import torch.nn as nn
import math


# --- 占位符模块,将在后续小节中实现 --- (1)Encoder-Decoder 整体结构

# class PositionalEncoding(nn.Module):
#   """
#   位置编码模块
#   """
#
#   def forward(self, x):
#     pass
### (4)残差连接与层归一化 , 3.1.2.5 位置编码
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)


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


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

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)
    ## Q: [batch_size, num_heads, seq_len_q, d_k]
    ## K: [batch_size, num_heads, seq_len_k, d_k]
    ## attn_scores: [batch_size, num_heads, seq_len_q, seq_len_k]
    attn_scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)

    # 2. 应用掩码 (如果提供)
    if mask is not None:
      ## # 调整掩码维度以匹配注意力得分: [batch_size, 1, seq_len_q, seq_len_k]
      ## # 确保掩码广播到所有head
      if mask.dim() == 3:
          mask = mask.unsqueeze(1)  # [batch_size, 1, seq_len, seq_len]
      ## # 只保留与注意力得分匹配的维度
      mask = mask[:, :, :attn_scores.size(2), :attn_scores.size(3)]
      # 将掩码中为 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 output


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

### (3)前馈神经网络
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 x


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

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.self_attn = MultiHeadAttention(d_model, num_heads)  # 补全参数
    self.feed_forward = PositionWiseFeedForward(d_model, d_ff, dropout)  # 补全参数

    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(d_model, num_heads)  # 补全参数
    self.cross_attn = MultiHeadAttention(d_model, num_heads)  # 补全参数
    self.feed_forward = PositionWiseFeedForward(d_model, d_ff, dropout)  # 补全参数
    # 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):
  def forward(self, x, encoder_output, src_mask=None, tgt_mask=None):
    # 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 x


##### add main

def create_mask(seq_len, batch_size, device, mask_type="pad"):
  """
  创建掩码矩阵
  Args:
      seq_len: 序列长度
      batch_size: 批次大小
      device: 设备
      mask_type: "pad" (填充掩码) 或 "lookahead" (前瞻掩码)
  Returns:
      mask: [batch_size, seq_len, seq_len]
  """
  if mask_type == "pad":
    # 填充掩码 (全部为1,表示所有位置都可见)
    mask = torch.ones(batch_size, seq_len, seq_len, device=device)
  elif mask_type == "lookahead":
    # 前瞻掩码 (下三角矩阵,防止看到未来的token)
    mask = torch.tril(torch.ones(batch_size, seq_len, seq_len, device=device))
  return mask


def main():
  # 设置测试参数
  batch_size = 2  # 批次大小
  seq_len_src = 5  # 源序列长度
  seq_len_tgt = 4  # 目标序列长度
  d_model = 512  # 模型维度
  num_heads = 8  # 注意力头数
  d_ff = 2048  # 前馈网络隐藏层维度
  dropout = 0.1  # dropout 概率

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

  # ==================== 测试 1: 位置编码 ====================
  print("\n" + "=" * 50)
  print("测试 1: 位置编码 (PositionalEncoding)")
  print("=" * 50)

  # 创建位置编码实例
  pos_encoder = PositionalEncoding(d_model, dropout).to(device)

  # 创建测试输入 (batch_size, seq_len, d_model)
  x = torch.randn(batch_size, seq_len_src, d_model).to(device)

  # 前向传播
  x_pos = pos_encoder(x)

  print(f"输入形状: {x.shape}")
  print(f"输出形状: {x_pos.shape}")
  print(f"位置编码矩阵形状: {pos_encoder.pe.shape}")

  # ==================== 测试 2: 多头注意力 ====================
  print("\n" + "=" * 50)
  print("测试 2: 多头注意力 (MultiHeadAttention)")
  print("=" * 50)

  # 创建多头注意力实例
  multi_head_attn = MultiHeadAttention(d_model, num_heads).to(device)

  # 创建测试输入
  Q = torch.randn(batch_size, seq_len_src, d_model).to(device)
  K = torch.randn(batch_size, seq_len_src, d_model).to(device)
  V = torch.randn(batch_size, seq_len_src, d_model).to(device)

  # 创建掩码 [batch_size, seq_len, seq_len]
  mask = create_mask(seq_len_src, batch_size, device, "pad")

  # 前向传播
  attn_output = multi_head_attn(Q, K, V, mask)

  print(f"Q/K/V 形状: {Q.shape}")
  print(f"掩码形状: {mask.shape}")
  print(f"注意力输出形状: {attn_output.shape}")

  # ==================== 测试 3: 位置前馈网络 ====================
  print("\n" + "=" * 50)
  print("测试 3: 位置前馈网络 (PositionWiseFeedForward)")
  print("=" * 50)

  # 创建前馈网络实例
  ff_network = PositionWiseFeedForward(d_model, d_ff, dropout).to(device)

  # 创建测试输入
  x = torch.randn(batch_size, seq_len_src, d_model).to(device)

  # 前向传播
  ff_output = ff_network(x)

  print(f"输入形状: {x.shape}")
  print(f"输出形状: {ff_output.shape}")

  # ==================== 测试 4: 编码器层 ====================
  print("\n" + "=" * 50)
  print("测试 4: 编码器层 (EncoderLayer)")
  print("=" * 50)

  # 创建编码器层实例
  encoder_layer = EncoderLayer(d_model, num_heads, d_ff, dropout).to(device)

  # 创建测试输入和掩码
  src_input = torch.randn(batch_size, seq_len_src, d_model).to(device)
  src_mask = create_mask(seq_len_src, batch_size, device, "pad")

  # 前向传播
  encoder_output = encoder_layer(src_input, src_mask)

  print(f"编码器输入形状: {src_input.shape}")
  print(f"编码器掩码形状: {src_mask.shape}")
  print(f"编码器输出形状: {encoder_output.shape}")

  # ==================== 测试 5: 解码器层 ====================
  print("\n" + "=" * 50)
  print("测试 5: 解码器层 (DecoderLayer)")
  print("=" * 50)

  # 创建解码器层实例
  decoder_layer = DecoderLayer(d_model, num_heads, d_ff, dropout).to(device)

  # 创建测试输入
  tgt_input = torch.randn(batch_size, seq_len_tgt, d_model).to(device)

  # 创建掩码
  src_mask = create_mask(seq_len_src, batch_size, device, "pad")  # 源序列掩码 [2,5,5]
  tgt_mask = create_mask(seq_len_tgt, batch_size, device, "lookahead")  # 目标序列掩码 [2,4,4]

  # 前向传播
  decoder_output = decoder_layer(tgt_input, encoder_output, src_mask, tgt_mask)

  print(f"解码器输入形状: {tgt_input.shape}")
  print(f"编码器输出形状: {encoder_output.shape}")
  print(f"源序列掩码形状: {src_mask.shape}")
  print(f"目标序列掩码形状: {tgt_mask.shape}")
  print(f"解码器输出形状: {decoder_output.shape}")

  # ==================== 综合测试 ====================
  print("\n" + "=" * 50)
  print("测试 6: 综合测试 (完整流程)")
  print("=" * 50)

  # 1. 创建所有组件
  pos_encoder = PositionalEncoding(d_model, dropout).to(device)
  encoder_layer = EncoderLayer(d_model, num_heads, d_ff, dropout).to(device)
  decoder_layer = DecoderLayer(d_model, num_heads, d_ff, dropout).to(device)

  # 2. 源序列处理
  src_emb = torch.randn(batch_size, seq_len_src, d_model).to(device)  # 源序列词嵌入
  src_emb_pos = pos_encoder(src_emb)  # 添加位置编码
  src_mask = create_mask(seq_len_src, batch_size, device, "pad")
  encoder_out = encoder_layer(src_emb_pos, src_mask)

  # 3. 目标序列处理
  tgt_emb = torch.randn(batch_size, seq_len_tgt, d_model).to(device)  # 目标序列词嵌入
  tgt_emb_pos = pos_encoder(tgt_emb)  # 添加位置编码
  tgt_mask = create_mask(seq_len_tgt, batch_size, device, "lookahead")
  decoder_out = decoder_layer(tgt_emb_pos, encoder_out, src_mask, tgt_mask)

  print(f"源序列词嵌入形状: {src_emb.shape}")
  print(f"添加位置编码后形状: {src_emb_pos.shape}")
  print(f"编码器输出形状: {encoder_out.shape}")
  print(f"解码器输出形状: {decoder_out.shape}")
  print("\n✅ 所有测试通过!")


if __name__ == "__main__":
  main()

image


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>']

从 N-gram 到 RNN -> Transformer 架构 -> Decoder-Only 架构 ->

  • Transformer的设计哲学是“先理解,再生成”。
  • Decoder-Only :完全抛弃了编码器,只保留了解码器部分。【 工作模式被称为自回归 (Autoregressive) 】
  • 掩码自注意力 (Masked Self-Attention) : 保证在预测第 t 个词时,不去“偷看”第 t+1 个词的答案。

image

提示 (Prompt) 与 模型 沟通的语言:Temperature这类的可配置参数,其本质是通过调整模型对 “概率分布” 的采样策略,让输出匹配具体场景需求,配置合适的参数可以提升Agent在特定场景的性能。
image

ps: 想起当初 Microsoft Copilot 允许用户切换“创意”(Creative)、“平衡”(Balanced)和“精确”(Precise)三种模式,呈现不同的聊天内容,想必应该就是这个T。

零样本、单样本与少样本提示:给模型提供示例(Exemplar)的数量,提示可以分为三种类型。

  • 零样本提示 (Zero-shot Prompting) :不给模型任何示例,直接让它根据指令完成任务。
  • 单样本提示 (One-shot Prompting) :给模型提供一个完整的示例,向它展示任务的格式和期望的输出风格。
  • 少样本提示 (Few-shot Prompting) :提供多个示例,让模型更准确地理解任务的细节、边界和细微差别,获得更好的性能。

指令调优 (Instruction Tuning) 是一种微调技术,它使用大量“指令-回答”格式的数据对预训练模型进行进一步的训练。经过指令调优后,模型能更好地理解并遵循用户的指令。

  • 对“文本补全”模型的提示(你需要用少样本提示“教会”模型做什么)。
  • 对“指令调优”模型的提示(你可以直接下达指令)。
  • 角色扮演 (Role-playing) 通过赋予模型一个特定的角色,我们可以引导它的回答风格、语气和知识范围,使其输出更符合特定场景的需求。
  • 上下文示例 (In-context Example) 这与少样本提示的思想一致,通过在提示中提供清晰的输入输出示例,来“教会”模型如何处理我们的请求,尤其是在处理复杂格式或特定风格的任务时非常有效。
  • 对于需要逻辑推理、计算或多步骤思考的复杂问题,直接让模型给出答案往往容易出错。思维链 (Chain-of-Thought, CoT) 是一种强大的提示技巧,它通过引导模型“一步一步地思考”,提升了模型在复杂任务上的推理能力。
    -- 实现 CoT 的关键,是在提示中加入一句简单的引导语,如“请逐步思考”或“Let's think step by step”。(通过显式地展示其推理过程,模型不仅更容易得出正确的答案,也让它的回答变得更可信、更易于我们检查和纠正。)

将自然语言文本喂给大语言模型之前,必须先将其转换成模型能够处理的数字格式。

  • 将文本序列转换为数字序列的过程,就叫做分词 (Tokenization) 。
  • 分词器 (Tokenizer) 的作用是定义一套规则,将原始文本切分成一个个最小的单元,【词元 (Token)】 。
    -- 按词分词 (Word-based) ;
    -- 按字符分词 (Character-based) ;

为了兼顾词表大小和语义表达,现代大语言模型普遍采用子词分词 (Subword Tokenization) 算法。它的核心思想是:将常见的词(如 "agent")保留为完整的词元,同时将不常见的词(如 "Tokenization")拆分成多个有意义的子词片段(如 "Token" 和 "ization")。这样既控制了词表的大小,又能让模型通过组合子词来理解和生成新词。
字节对编码 (Byte-Pair Encoding, BPE) 是最主流的子词分词算法之一[6],GPT系列模型就采用了这种算法。
假设我们的迷你语料库是 {"hug": 1, "pug": 1, "pun": 1, "bun": 1},并且我们想构建一个大小为 10 的词表。
image

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

意义

  • 上下文窗口限制:模型的上下文窗口(如 8K, 128K)是以 Token 数量计算的,而不是字符数或单词数。同样一段话,在不同语言(如中英文)或不同分词器下,Token 数量可能相差巨大。精确管理输入长度、避免超出上下文限制是构建长时记忆智能体的基础。

ps: 想起了最新的 TOON 协议 和JSON的对比。

  • API 成本:大多数模型 API 都是按 Token 数量计费的。了解你的文本会被如何分词,是预估和控制智能体运行成本的关键一步。
  • 模型表现的异常:有时模型的奇怪表现根源在于分词。例如,模型可能很擅长计算 2 + 2,但对于 2+2(没有空格)就可能出错,因为后者可能被分词器视为一个独立的、不常见的词元。同样,一个词因为首字母大小写不同,也可能被切分成完全不同的 Token 序列,从而影响模型的理解。
  • 在设计提示词和解析模型输出时,考虑到这些“陷阱”有助于提升智能体的鲁棒性。
    pip install transformers torch
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("模型和分词器加载完成!")
##############

# 准备对话输入
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')}
###################################
# 使用模型生成回答
# 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)

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

image

这是否意味着 "意识" 被创造了出来?

image

这是不是 有点像 "碳基生命"被"逼疯"了的样子?

  1. 检索增强生成 (Retrieval-Augmented Generation, RAG) [14]: 这是目前缓解幻觉的有效方法之一。RAG 系统通过在生成之前从外部知识库(如文档数据库、网页)中检索相关信息,然后将检索到的信息作为上下文,引导模型生成基于事实的回答。
  2. 多步推理与验证: 引导模型进行多步推理,并在每一步进行自我检查或外部验证。
  3. 引入外部工具: 允许模型调用外部工具(如搜索引擎、计算器、代码解释器)来获取实时信息或进行精确计算。
  • 模型演进与核心架构:本章追溯了从统计语言模型 (N-gram) 到神经网络模型 (RNN, LSTM),再到奠定现代 LLM 基础的 Transformer 架构。通过“自顶向下”的代码实现,本章拆解了 Transformer 的核心组件,并阐述了自注意力机制在并行计算和捕捉长距离依赖中的关键作用。

  • 与模型的交互方式:本章介绍了与 LLM 交互的两个核心环节:提示工程 (Prompt Engineering) 和文本分词 (Tokenization)。前者用于指导模型的行为,后者是理解模型输入处理的基础。通过本地部署并运行开源模型的实践,将理论知识应用于实际操作。

  • 模型生态与选型:本章系统地梳理了为智能体选择模型时需要权衡的关键因素,并概览了以 OpenAI GPT、Google Gemini 为代表的闭源模型和以 Llama、Mistral 为代表的开源模型的特点与定位。

  • 法则与局限:本章探讨了驱动 LLM 能力提升的缩放法则,阐述了其背后的基本原理。同时,本章也分析了模型存在的如事实幻觉、知识过时等固有局限性,这对于构建可靠、鲁棒的智能体至关重要。
    image

  • 模型无法准确判断哪些是有效输入、哪些是填充的无效内容,可能导致生成结果异常(比如乱码、逻辑混乱、重复输出等),建议手动传入注意力掩码(attention_mask)来保证结果可靠。

  1. 关键背景:你的模型(Qwen1.5-0.5B-Chat)中,「填充标记(pad token,用来补全长度不一致的输入文本)」和「结束标记(eos token,用来标识文本结束)」是同一个东西。
  2. 为什么出问题:模型生成文本时,需要靠「注意力掩码(attention_mask)」区分 “有效输入内容”(比如你的问题 “你好,请介绍你自己”)和 “填充的无效内容”(比如为了统一输入长度补的空白标记)。现在因为两个标记一样,模型没法自动推断出注意力掩码,就会 “分不清” 有效内容和填充内容,进而可能出现奇怪的生成结果。
  3. 你的代码小特殊:从你打印的 model_inputs 能看到,其实分词器已经自动生成了 attention_mask(全是 1,说明你的输入没有填充内容,全是有效内容),所以你当前的生成结果是正常的,但这个警告是提醒你 “如果后续有填充输入,不手动传掩码就会出问题”。
方案 1:最简单!生成时直接传入已有的 attention_mask
###################################
# 使用模型生成回答
# max_new_tokens 控制了模型最多能生成多少个新的Token
# generated_ids = model.generate(
#     model_inputs.input_ids,
#     max_new_tokens=512
# )
# 使用模型生成回答(新增 attention_mask 参数),修复 The attention mask is not set and cannot be inferred from input because pad token is same as eos token. As a consequence, you may observe unexpected behavior. Please pass your input's `attention_mask` to obtain reliable results.
generated_ids = model.generate(
    model_inputs.input_ids,
    attention_mask=model_inputs.attention_mask,  # 关键:传入注意力掩码
    max_new_tokens=512
)
方案 2:指定分词器的 pad_token,让 pad token 和 eos token 不一样 【无效】
# 加载分词器
tokenizer = AutoTokenizer.from_pretrained(model_id)
# 新增:指定 pad token,解决“pad和eos相同”的问题
tokenizer.pad_token = tokenizer.eos_token  # 关键代码,Qwen 模型优先推荐这种写法
方案 3:最规范!同时指定 pad token + 传入 attention_mask

结合方案 1 和方案 2,既解决根源问题,又保证生成时的严谨性,适合后续批量处理、有填充输入的场景(比如一次传入多个长度不一致的问题)。

# 1. 加载分词器并指定 pad token
tokenizer = AutoTokenizer.from_pretrained(model_id)
tokenizer.pad_token = tokenizer.eos_token  # 解决 pad 和 eos 相同的问题

# 2. 生成时传入 attention_mask
generated_ids = model.generate(
    model_inputs.input_ids,
    attention_mask=model_inputs.attention_mask,  # 传入掩码
    max_new_tokens=512
)

习题

  1. 马尔可夫假设的含义
    马尔可夫假设是 N-gram 模型的核心理论基础,其核心思想是:一个单词的出现概率仅依赖于其前面有限个(N-1 个)最近的单词,而与更早的单词无关。
  2. N-gram 模型的根本性局限
    数据稀疏问题:随着 N 的增大,N-gram 组合的数量呈指数级增长,语料库中大量合理的 N-gram 组合从未出现,导致概率为 0,即使添加平滑处理也无法从根本上解决。
  3. 上下文窗口有限:马尔可夫假设忽略了长距离依赖关系,无法捕捉句子中相隔较远单词之间的语义关联,而这种关联在自然语言中普遍存在(如指代关系、逻辑关系等)。
  4. 参数空间爆炸:N-gram 模型需要存储所有可能的 N-gram 组合的概率,当 N 增大或词汇量增加时,参数数量会急剧膨胀,导致模型存储和计算成本过高。
  5. 缺乏语义理解:N-gram 仅基于单词的共现频率计算概率,不理解单词的语义含义,无法处理同义词、多义词等语义层面的语言现象。
  6. RNN/LSTM 的改进与优势
    (1)如何克服 N-gram 局限
    解决长距离依赖(部分):RNN 通过循环结构将前一时刻的隐藏状态传递到当前时刻,理论上可以捕捉任意长度的上下文依赖;LSTM 通过门控机制(输入门、遗忘门、输出门)进一步解决了 RNN 的梯度消失问题,能够有效捕捉较长距离的语义关联。
    参数共享:RNN/LSTM 在每个时间步共享相同的参数,避免了 N-gram 参数空间爆炸的问题,模型参数数量不再随词汇量和上下文长度呈指数增长。
    语义表征:模型通过训练学习到单词的分布式语义表征(词向量),能够捕捉单词的语义信息,更好地处理同义词、多义词等语义现象。
    (2)优势
    序列建模能力:天然适合处理序列数据,能够捕捉序列中的时序依赖关系,在语音识别、机器翻译等时序任务中表现出色。
    参数效率高:参数共享机制使得模型在处理长序列时,计算成本不会显著增加,训练和推理效率优于高阶 N-gram 模型。
  7. Transformer 的改进与优势
    (1)如何克服 N-gram 局限
  • 全局上下文依赖:通过自注意力机制,Transformer 能够直接计算序列中任意两个单词之间的关联,不受距离限制,完美解决了 N-gram 和 RNN/LSTM 的上下文窗口有限问题。
  • 并行计算:摒弃了 RNN 的循环结构,采用全连接和注意力机制,使得模型可以并行处理整个序列,大幅提升了训练和推理效率。
  • 更强的语义表征:通过多层 Transformer 堆叠和自注意力机制,模型能够学习到更复杂、更抽象的语义表征,捕捉语言中的深层语义结构。
    (2)优势
  • 并行效率极高:相比 RNN 的串行处理,Transformer 的并行计算能力使其能够利用现代 GPU 的算力优势,处理更长的序列和更大规模的语料。
  • 长距离依赖建模能力:自注意力机制可以直接建立任意位置单词的关联,无需像 LSTM 那样逐步传递信息,在长文本处理任务中表现远超传统模型。
  • 模型扩展性强:通过堆叠更多的 Transformer 层和扩大模型参数规模,可以持续提升模型的性能,这也是大语言模型能够取得突破性进展的核心原因。

二 、Transformer 架构核心问题解析

  1. 自注意力机制(Self-Attention)的核心思想
  • 自注意力机制的核心思想是:在处理序列中的每个位置时,通过计算该位置与序列中所有其他位置的关联权重,动态聚合整个序列的信息,生成该位置的上下文表征。
  • 其本质是为每个单词生成一个 "注意力权重向量",表示该单词与其他单词的关联程度,然后通过加权求和的方式,将其他单词的信息融合到当前单词的表征中。这种机制使得模型能够根据当前语境,灵活地关注序列中最相关的信息,不受单词位置距离的限制。
  1. Transformer 并行处理与位置编码的作用
    (1)并行处理的原因
  • RNN 的串行本质:RNN 的计算依赖于前一时刻的隐藏状态,必须按顺序逐个处理序列中的单词,前一个单词处理完成后才能开始处理下一个,无法并行。
  • Transformer 的并行设计:Transformer 采用的是全连接层和自注意力机制,每个位置的计算仅依赖于输入序列的全局信息,不同位置之间的计算相互独立,因此可以同时处理序列中的所有位置,实现并行计算。
    (2)位置编码的作用
  • Transformer 的自注意力机制本身不包含序列顺序信息,所有位置的计算是并行且对称的,无法区分单词在序列中的位置。位置编码的作用就是为每个位置的单词添加一个独特的位置信息,让模型能够感知到单词在序列中的顺序和相对位置。
  • 位置编码通常通过三角函数或可学习的向量实现,将位置信息与单词的词向量相加,使得模型在计算注意力时能够考虑到单词的位置差异,从而正确建模语言中的时序关系。
  1. Decoder-Only 与 Encoder-Decoder 架构的区别及主流选择原因
    (1)架构区别
维度 Decoder-Only 架构 Encoder-Decoder 架构
结构组成 仅由多层 Decoder 堆叠而成 由 Encoder 和 Decoder 两部分组成
输入处理 输入序列直接输入 Decoder 输入序列先经过 Encoder 编码为上下文向量,再输入 Decoder
核心机制 采用自回归的方式生成输出,通过 Masked 自注意力防止看到未来信息 Encoder 使用自注意力,Decoder 使用 Masked 自注意力和 Encoder-Decoder 注意力,关注 Encoder 的输出
适用任务 文本生成、对话生成、代码生成等 机器翻译、文本摘要、信息抽取等

(2)主流选择 Decoder-Only 的原因
训练和推理效率更高:Decoder-Only 架构无需维护 Encoder 和 Decoder 两部分参数,模型结构更简洁,训练时的计算成本更低,推理时的速度更快。
生成能力更强:自回归生成方式更符合自然语言的生成逻辑,能够生成连贯、流畅的长文本,在对话、创作等生成式任务中表现更出色。
多任务泛化能力:Decoder-Only 模型通过提示词工程,可以适配多种任务(如分类、摘要、推理等),无需针对不同任务修改模型结构,灵活性更高。
工程实现更简单:Decoder-Only 架构的训练和推理流程更统一,更容易进行分布式训练和优化,降低了工程实现的复杂度。

五、文本子词分词算法核心问题

  1. 不能直接以字符或单词作为输入单元的原因
    (1)字符作为输入单元的问题
    语义粒度太细:字符本身不具备完整的语义,需要模型学习大量的组合规则才能形成有意义的单词和句子,增加了模型的学习难度和训练成本。
    序列长度过长:以字符为单位会导致输入序列长度大幅增加,不仅增加了计算量,还会加剧长文本处理时的上下文窗口限制问题。
    (2)单词作为输入单元的问题
    词汇量爆炸:语言中的单词数量庞大,且不断有新词出现(如网络热词、专业术语),模型无法覆盖所有单词,导致未登录词(OOV)问题严重。
    语义冗余:很多单词存在词形变化(如英语的时态、单复数)或语义关联(如 "run" 和 "running"),以单词为单位会忽略这些关联,增加模型的参数数量和学习负担。
  2. BPE(Byte Pair Encoding)算法解决的问题
    BPE 算法是一种基于统计的子词分词算法,核心是通过迭代合并出现频率最高的字符或子词对,生成新的子词单元,直到达到预设的词汇量。
    它主要解决了以下问题:
    未登录词问题:BPE 可以将未见过的单词拆分为已学习的子词单元,从而处理词典外的新词,大幅降低 OOV 比例。
    语义关联建模:BPE 能够捕捉单词的词形变化和语义关联(如将 "running" 拆分为 "run" 和 "ing"),让模型能够共享这些子词的语义信息,提升模型的泛化能力和参数效率。
    平衡语义粒度:子词单元介于字符和单词之间,既保留了单词的部分语义信息,又具备一定的灵活性,能够在语义表达和计算效率之间取得平衡。
    pip install hf_xet
from transformers import AutoTokenizer, AutoModelForCausalLM

# 加载模型和分词器
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B")

# 生成文本
prompt = "自然语言处理的核心任务包括"
inputs = tokenizer(prompt, return_tensors="pt")
outputs = model.generate(
    **inputs,
    max_new_tokens=100,
    temperature=0.7,
    top_p=0.9,
    do_sample=True
)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))

image

采样参数对输出的影响
Temperature(温度):控制输出的随机性。温度越高(如 1.5),输出越多样化、富有创意,但可能出现逻辑混乱;温度越低(如 0.1),输出越保守、确定,更倾向于选择概率最高的词,但可能缺乏多样性。
image
image
Top-p(核采样):选择概率累计和达到 p 的最可能的词集合,仅从该集合中采样。Top-p 越小(如 0.5),输出越集中、确定;Top-p 越大(如 0.95),输出越多样化。
image
image
Do_sample:是否启用采样策略。若为 False,则采用贪心搜索,每次选择概率最高的词,输出确定性强但缺乏多样性。
image

The following generation flags are not valid and may be ignored: ['temperature', 'top_p', 'top_k']. Set `TRANSFORMERS_VERBOSITY=info` for more details.

你设置的 temperature、top_p 这两个采样相关参数无效,模型会忽略它们

  1. 在 model.generate() 中设置了 do_sample=False(这是默认值,即使不显示指定也会生效),此时模型采用的是「贪心搜索(Greedy Search)」策略 —— 每次生成时只选择概率最高的下一个 token,不存在随机采样的过程。
  2. temperature(控制采样随机性)、top_p(核采样,限定采样候选集)是仅在随机采样模式下生效的参数,贪心搜索不需要这些参数来控制随机性,因此 Transformers 库会提示这些参数无效并忽略。
  3. 提示中还提到了 top_k(虽未在你的代码中设置),它同样是采样模式的专属参数,与 temperature、top_p 作用类似,仅在 do_sample=True 时有效。
  • 贪心搜索(do_sample=False):输出确定性强,每次输入相同的 prompt 会得到几乎一致的结果,但可能缺乏多样性,甚至出现重复文本。
  • 随机采样(do_sample=True):结合 temperature 和 top_p 可平衡多样性与合理性:
    -- temperature 越大(如 1.0),随机性越强;越小(如 0.3),输出越保守、确定。
    -- top_p 越小(如 0.7),采样候选集越窄,输出越稳定;越大(如 0.95),候选集越宽,多样性越高。
    image
  1. 不同提示策略效果对比(以文本分类任务为例)
    任务:将句子 "手机续航能力强,拍照效果好,值得购买" 分类为 "正面评价" 或 "负面评价"。
    Zero-shot:直接给出分类指令,无需示例。
    提示词:"判断以下句子的情感倾向,分为正面评价或负面评价:手机续航能力强,拍照效果好,值得购买"
    效果:模型能够理解任务,但在复杂语境下准确率可能较低。
    image
    image
    Few-shot:提供少量示例,帮助模型理解任务。
    提示词:" 判断以下句子的情感倾向,分为正面评价或负面评价:
    这款电脑性能差,经常卡顿,不推荐购买 - 负面评价
    耳机音质清晰,佩戴舒适,性价比很高 - 正面评价
    手机续航能力强,拍照效果好,值得购买 - "
    效果:通过示例引导,模型准确率显著提升,适用于训练数据较少的场景。
    image
    image

Chain-of-Thought(CoT):引导模型逐步推理,给出分类理由。
提示词:"判断以下句子的情感倾向,分为正面评价或负面评价,并说明理由:手机续航能力强,拍照效果好,值得购买"
效果:模型能够给出更合理的分类结果,且输出具有可解释性,适用于需要逻辑推理的复杂任务。
image

模型幻觉缓解方法解析

  1. 检索增强生成(RAG)的工作原理与适用场景
    (1)工作原理
    RAG 的核心思想是:在模型生成文本之前,先从外部知识库中检索与当前问题相关的信息,将检索结果作为上下文输入给模型,引导模型基于真实信息生成输出,从而减少幻觉。
    其流程通常包括:
    问题解析:对用户的问题进行语义理解和关键词提取。
    信息检索:通过向量数据库等工具,从知识库中检索与问题相关的文档或片段。
    上下文构建:将检索到的信息与用户问题拼接,形成新的提示词。
    文本生成:模型基于构建好的上下文生成回答,确保输出内容与检索结果一致。
    (2)适用场景
    知识密集型任务:如医疗咨询、法律问答、技术支持等,需要基于准确的专业知识生成回答。
    时效性强的任务:如新闻问答、行业动态咨询等,需要结合最新的外部信息,弥补模型训练数据的滞后性。
    事实性要求高的任务:如学术问答、产品介绍等,要求输出内容必须真实可靠,不容许虚构信息。
  2. 前沿幻觉缓解方法调研
    除了 RAG、多步推理和外部工具调用,当前前沿研究还提出了以下方法:
    模型对齐(Model Alignment):通过 RLHF(基于人类反馈的强化学习)、DPO(直接偏好优化)等方法,让模型的输出更符合人类的事实性和真实性偏好,从模型内部减少幻觉的产生。优势是无需依赖外部知识库,可提升模型在各类任务中的事实准确性。
    事实性验证模块:在模型生成输出后,添加一个独立的验证模块,检查输出内容是否与外部知识库或事实一致,对存在幻觉的内容进行修正或标记。优势是能够对生成结果进行事后校验,提升输出的可靠性,适用于对准确性要求极高的场景。
    结构化输出约束:通过提示词或模型微调,引导模型生成结构化的输出(如表格、列表),并要求输出内容必须有明确的来源依据。优势是输出格式规范,便于人工审核和自动化验证,减少模型自由发挥导致的幻觉。
    多模型协同:将多个不同的模型结合使用,让一个模型生成回答,另一个模型进行事实性校验,或者让多个模型共同投票决定最终结果。优势是能够利用不同模型的互补性,降低单一模型产生幻觉的概率,提升输出的鲁棒性。
    八、论文辅助阅读智能体设计
  3. 基座模型选择及考虑因素
    推荐选择:Llama 3-70B 或 Qwen 3-72B(开源大模型)
    选择时需要考虑的核心因素:
    上下文窗口:学术论文通常篇幅较长(数千至数万单词),需要模型支持足够长的上下文窗口(至少 32k tokens),才能完整处理单篇论文。
    语义理解能力:学术论文包含复杂的逻辑结构、专业术语和数学公式,需要模型具备强大的语义理解和逻辑推理能力,能够准确提取核心信息和分析论文观点。
    可扩展性:支持微调、模型蒸馏等优化手段,能够针对学术论文阅读场景进行定制化训练,提升模型在特定领域的表现。
    开源性:支持本地部署和二次开发,便于集成检索模块、文档解析模块等功能,构建完整的智能体系统。
    多语言支持:能够处理中英文等多语言论文,满足不同研究人员的需求。
  4. 提示词设计与长文本处理
    (1)提示词设计策略
    结构化提示:采用 "任务指令 + 输出格式要求 + 示例" 的结构化提示词,引导模型按照固定格式输出结果。例如:"请总结以下论文的核心内容,包括研究背景、研究方法、实验结果和结论,输出格式为:1. 研究背景:... 2. 研究方法:... 3. 实验结果:... 4. 结论:..."
    分任务提示:将复杂任务拆解为多个子任务,逐步引导模型完成。例如,先让模型提取论文的研究问题,再提取研究方法,最后整合为完整总结。
    专业术语引导:在提示词中明确提及论文所属领域的专业术语,帮助模型快速定位核心信息,提升理解准确率。
    (2)长文本处理方案
    文本分段与摘要:将长论文按照章节或段落拆分为多个片段,先对每个片段生成摘要,再将所有摘要整合为论文的完整总结,避免上下文窗口限制。
    关键信息提取优先:先从论文中提取标题、摘要、关键词、图表说明等核心信息,再基于这些信息理解论文的整体框架,最后针对具体章节进行详细分析。
    检索增强阅读:将论文内容构建为向量数据库,当用户提问时,先检索与问题相关的论文片段,再让模型基于这些片段生成回答,无需处理整篇论文。
  5. 确保输出准确客观的系统设计
    引用溯源机制:要求模型在生成回答时,标注信息来源的论文章节、页码或段落,方便用户核对原文,确保信息的可追溯性。
    事实性校验模块:集成外部学术数据库(如 Google Scholar、CNKI)或事实性验证模型,对模型生成的信息进行实时校验,标记可能存在的错误或幻觉内容。
    人工审核接口:提供人工审核功能,对于重要的论文解读结果,允许研究人员进行修改和确认,确保输出内容符合学术严谨性要求。
    领域专家微调:邀请相关领域的专家标注高质量的论文解读数据,对模型进行微调,提升模型在特定领域的准确性和专业性。
    输出格式约束:强制模型采用结构化的输出格式(如列表、表格),避免模糊性表述,让输出内容更清晰、更易于验证。

task04 - task08

see: https://chuna2.787528.xyz/Chary/p/19410406/hello-agents_by_wx20190202gyb-prart2

posted @ 2025-12-27 23:31  CharyGao  阅读(71)  评论(0)    收藏  举报