Hexo

凡事预则立,不预则废


  • Home

  • Tags

  • Archives

  • Navigation

  • Search

NLP——VeRL源码阅读笔记

注:本文包含 AI 辅助创作

  • 注:本文的版本包含一些很早的代码(部分小节可能有点过期了),也有很多是主要是截止到 20260530 日的 verl 版本 (commit_id=9f73954a87e247de4c31bc2f5222969e395b2904)

VeRL 中,RayWorkerGroup 函数调用路径理解

  • 以 update_weights 完整调用链为例
    • RayWorkerGroup 实例的 update_weights 方法在底层 Worker 类上被 @register(dispatch_mode=Dispatch.ONE_TO_ALL, blocking=False) 装饰
      • 这意味着 RayWorkerGroup 会通过 func_generator 动态生成一个包装函数
    • 在 func_generator 中,调用流程为:
      • dispatch_fn → dispatch_one_to_all(透传参数)
      • execute_fn → execute_all → execute_all_async
      • execute_all_async 内部为每个 worker 调用 _execute_remote_single_worker,该方法返回 remote_call.remote(...)**,即 **ray.ObjectRef
      • 最终返回 [ray.ObjectRef, ray.ObjectRef, ...] — 一个 ray.ObjectRef 列表
      • 由于 blocking=False,ray.get() 不会在包装函数内执行
      • collect_fn → collect_all_to_all,直接透传返回(仍然是一个列表)
    • 所以最终,RayWorkerGroup.update_weights 函数得到的结果是一个 ray.ObjectRef 列表

VeRL 中,use_dynamic_bsz 参数解读

  • 动态 Batch Size 一般是指通过平衡每张卡上的 Attention Token(包括 Prompt 和 Response),防止出现现存爆炸而设计的策略

  • 动态 Batch Size 这个策略只影响如何将 mini-batch 拆分为 micro-batch,不影响 train_batch_size 拆分为 mini-batch 的方式

  • 从数学上看,这个策略本身并不修改梯度的期望(数学上使用前后等价),但是从实践上看,因为存在着一些浮点非结合律 + KK 重排样本 等导致并不能 bit-wise 等价

  • actor.use_dynamic_bsz 参数生效点:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    配置 (engine.yaml)
    ↓
    ActorWorker.train_batch / infer_batch 注入 (engine_workers.py)
    ↓ tu.assign_non_tensor(data, use_dynamic_bsz=..., max_token_len_per_gpu=...)
    engine.forward_backward_batch
    ↓
    prepare_micro_batches (engine/utils.py) ← 开关分叉点
    ├─ True: rearrange_micro_batches (seqlen_balancing.py:348)
    └─ False: tu.chunk_tensordict (固定 mbs 等分)
    ↓
    for micro_batch in micro_batches: ← 内层循环
    loss.backward() (或 forward_only)
    ↓
    postprocess_batch_func → restore_dynamic_batch ← 还原顺序
  • 一般做法:基于 KK 算法均衡划分 :

    • 使用下面的方法 估算 FLOPs,用 Karmarkar-Karp 多路划分让各 micro-batch 工作量接近
      $$ \text{calculate_workload} \approx 24576 \cdot \text{seq_len} + \text{seq_len}^2 $$
    • 24576 是根据标准的 7B dense transformer FLOPs 估算(Megatron-LM 论文 / Kaplan et al. 的推广形式)
    • 这个数字在其他场景一般也不修改,因为分配时是比较的方式进行的,常数项影响有限
  • 注:长序列下强烈建议开启,需要 bitwise 复现算法程序时注意这个参数是否都是打开或者关闭的

  • RL 中可以设置这个策略的地方有三个:

    • Actor 的前向后向过程
      • actor.use_dynamic_bsz / actor.ppo_max_token_len_per_gpu
    • 计算 \(\pi_\text{old}\) 的 log_prob 计算(forward-only)
      • rollout.log_prob_use_dynamic_bsz / rollout.log_prob_max_token_len_per_gpu
    • 计算 \(\pi_\text{ref}\) 的 log_prob 计算(forward-only)
      • ref.log_prob_use_dynamic_bsz / ref.log_prob_max_token_len_per_gpu

RayPPOTrainer → AsyncRolloutManager → AgentLoopWorker 链路梳理

  • 注:本节讲解代码 commit_id=9f73954a87e247de4c31bc2f5222969e395b2904
  • 整条链路是”Driver 编排 → CPU Worker 并发协调 → GPU Replica 推理“的三层结构
  • Driver 的 RayPPOTrainer 持有三个 Manager(LLMServerManager 管 GPU 推理引擎、RewardLoopManager 管奖励计算、AgentLoopManager 管生成编排),三者通过 LLMServerClient(轻量路由器)和 reward_loop_worker_handles(Ray actor 引用)连接
    • AgentLoopManager 持有 N 个纯 CPU 的 AgentLoopWorker(Ray actor),每个 Worker 在进程内用 asyncio 并发为每个样本实例化一个 AgentLoopBase 子类(SingleTurnAgentLoop/ToolAgentLoop)
    • AgentLoopBase 子类 通过 LLMServerClient → Ray RPC 调用 GPU 上的 vLLM replica 完成实际生成
  • 整体包含关系图
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    RayPPOTrainer (Driver, 单进程)
    │
    ├── owns: LLMServerManager ← 管理推理引擎生命周期
    │ ├── owns: global_load_balancer (Ray actor) ← 全局负载均衡
    │ ├── owns: rollout_replicas: list[RolloutReplica] (Ray actor, GPU)
    │ │ └── vLLMHttpServer / SGLangServer
    │ │ └── self.engine = AsyncLLM (进程内引擎)
    │ └── get_client() → LLMServerClient (轻量, 仅持有 load_balancer handle)
    │
    ├── owns: RewardLoopManager ← 管理奖励计算
    │ └── owns: reward_loop_workers: list[RewardLoopWorker] (Ray actor)
    │
    ├── owns: AgentLoopManager (= self.async_rollout_manager) ← 管理生成
    │ ├── holds: llm_client: LLMServerClient ← 从 LLMServerManager.get_client() 获得
    │ ├── holds: reward_loop_worker_handles ← 从 RewardLoopManager 获得(可选)
    │ └── owns: agent_loop_workers: list[AgentLoopWorker] (Ray actor, CPU)
    │ ├── holds: llm_client: LLMServerClient ← 共享同一个 client 引用
    │ ├── holds: reward_loop_worker_handles ← 流式 reward 时使用
    │ └── per-sample instantiate → AgentLoopBase (hydra.utils.instantiate)
    │ ├── SingleTurnAgentLoop (注册名 "single_turn_agent")
    │ └── ToolAgentLoop (注册名 "tool_agent")
    │ └── holds: server_manager: LLMServerClient
    │
    └── owns: CheckpointEngineManager
    ├── holds: trainer = actor_rollout_wg
    └── holds: replicas = rollout_replicas

各类的职责与创建时机

RayPPOTrainer (Driver)
  • verl/trainer/ppo/ray_trainer.py
  • 在 init_workers() 中按顺序创建链路上的所有组件:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    # 1. 创建 WorkerGroup (Actor/Critic/Ref, 已在前面梳理过)
    self.actor_rollout_wg = all_wg[str(actor_role)]

    # 2. 创建 RewardLoopManager
    self.reward_loop_manager = RewardLoopManager(config, rm_resource_pool)

    # 3. 创建 LLMServerManager (持有 vLLM replicas)
    self.llm_server_manager = LLMServerManager.create(
    config=self.config,
    worker_group=self.actor_rollout_wg, # 用于 colocation 权重同步
    rollout_resource_pool=actor_rollout_resource_pool
    )

    # 4. 创建 AgentLoopManager (= async_rollout_manager)
    self.async_rollout_manager = AgentLoopManager.create(
    config=self.config,
    llm_client=self.llm_server_manager.get_client(), # ← 关键: 注入 LLMServerClient
    teacher_client=...,
    reward_loop_worker_handles=reward_loop_worker_handles # ← 可选: 注入 RewardLoopWorker
    )

    # 5. 创建 CheckpointEngineManager
    self.checkpoint_manager = CheckpointEngineManager(
    trainer=self.actor_rollout_wg,
    replicas=self.llm_server_manager.get_replicas(),
    )
LLMServerManager → LLMServerClient
  • verl/workers/rollout/llm_server.py

  • LLMServerManager 负责:

    • 启动 vLLM/SGLang replicas (Ray actor,占 GPU)
    • 创建全局 load_balancer (Ray actor,做 least-inflight 负载均衡)
    • 提供 get_client() (./verl/workers/rollout/llm_server.py):返回一个轻量的 LLMServerClient 实例
      1
      2
      3
      4
      5
      6
      def get_client(self, client_cls=LLMServerClient, **kwargs) -> LLMServerClient:
      return client_cls(
      config=self.config,
      load_balancer_handle=self.global_load_balancer,
      **kwargs,
      )
  • 注意:LLMServerClient 本身不持有任何 GPU 资源 ,它只是一个”路由器”,持有 load_balancer 的 Ray actor handle

    • 多个 AgentLoopWorker 共享同一个 LLMServerClient 实例(或各自的副本),它们最终都路由到同一组 vLLM replicas
AgentLoopManager (= async_rollout_manager)
  • verl/experimental/agent_loop/agent_loop.py

  • 持有关系:

    1
    2
    3
    4
    5
    class AgentLoopManager:
    def __init__(self, config, llm_client, teacher_client, reward_loop_worker_handles):
    self.llm_client = llm_client # LLMServerClient 实例
    self.reward_loop_worker_handles = reward_loop_worker_handles # list[ActorHandle] 或 None
    self.agent_loop_workers_class = ray.remote(AgentLoopWorker)
  • 创建 Worker(./verl/experimental/agent_loop/agent_loop.py):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    async def _init_agent_loop_workers(self):
    self.agent_loop_workers = []
    num_workers = self.rollout_config.agent.num_workers # 默认 8, 纯 CPU
    for i in range(num_workers):
    node_id = node_ids[i % len(node_ids)] # round-robin 跨节点
    self.agent_loop_workers.append(
    self.agent_loop_workers_class.options(
    name=f"agent_loop_worker_{i}_...",
    scheduling_strategy=NodeAffinitySchedulingStrategy(node_id, soft=True)
    ).remote(
    self.config,
    self.llm_client, # ← 传给每个 worker
    self.teacher_client,
    self.reward_loop_worker_handles, # ← 传给每个 worker
    )
    )
    • AgentLoopWorker 是纯 CPU 的 Ray actor,分布在集群各节点上
      • 它们不直接持有 GPU,而是通过 llm_client 远程调用 GPU 上的 vLLM replicas
  • AgentLoopWorker 的 调用入口(./verl/experimental/agent_loop/agent_loop.py):

    1
    2
    3
    4
    5
    6
    7
    8
    async def generate_sequences(self, prompts: DataProto) -> DataProto:
    chunkes = prompts.chunk(len(self.agent_loop_workers)) # 按 worker 数切分
    outputs = await asyncio.gather(
    *[worker.generate_sequences.remote(chunk) # 并行 Ray RPC
    for worker, chunk in zip(self.agent_loop_workers, chunkes)]
    )
    output = DataProto.concat(outputs) # 合并结果
    return output
AgentLoopWorker (Ray actor, CPU)
  • verl/experimental/agent_loop/agent_loop.py

  • 持有关系:

    1
    2
    3
    4
    5
    6
    7
    class AgentLoopWorker:
    def __init__(self, config, llm_client, teacher_client, reward_loop_worker_handles):
    self.llm_client = llm_client # LLMServerClient (用于调 vLLM)
    self.reward_loop_worker_handles = reward_loop_worker_handles # 流式 reward
    self.tools = load_all_tools(...) # 工具列表
    self.tokenizer = ...
    self.processor = ...
  • 核心方法 generate_sequences (./verl/experimental/agent_loop/agent_loop.py):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    async def generate_sequences(self, batch: DataProto) -> DataProto:
    sampling_params = dict(temperature=..., top_p=..., top_k=..., ...)

    # 对 batch 中每个样本, 创建一个 asyncio Task
    tasks = []
    for i in range(len(batch)):
    tasks.append(asyncio.create_task(
    self._run_agent_loop(sample_sampling_params, trajectory_info[i], **kwargs)
    ))
    outputs = await asyncio.gather(*tasks) # 并发执行所有样本的 agent loop
    return self._postprocess(outputs)
    • 一个 AgentLoopWorker 处理一个 chunk(多个样本),每个样本有一个 agent loop,所有样本的 agent loop 都在 同一个 Ray actor 进程内并发执行(asyncio),而不是每样本一个 actor
      • 理解:一个 Worker 一个进程,对应一个 Ray actor,这个 Worker 负责的所有样本都并发进行(通过协程的方式)
  • _run_agent_loop 的实现 (./verl/experimental/agent_loop/agent_loop.py):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    async def _run_agent_loop(self, sampling_params, trajectory, *, agent_name, **kwargs):
    agent_loop_config = _agent_loop_registry[agent_name] # 从注册表取配置(包含 _target_ 字段)
    agent_loop = hydra.utils.instantiate( # 每次调用都新实例化一个 AgentLoop
    config=agent_loop_config,
    trainer_config=DictConfigWrap(config=self.config),
    server_manager=self.llm_client, # ← 注入 LLMServerClient
    tokenizer=self.tokenizer,
    processor=self.processor,
    dataset_cls=self.dataset_cls,
    data_config=...,
    tools=ToolListWrap(self.tools),
    )
    output = await agent_loop.run(sampling_params, **kwargs) # 执行 agent loop
    return await self._agent_loop_postprocess(output, ...)
AgentLoopBase (每样本实例化)
  • verl/experimental/agent_loop/agent_loop.py
  • 抽象基类,每个样本创建一个实例( _run_agent_loop 函数中通过 hydra.utils.instantiate),持有 server_manager: LLMServerClient
    子类 注册名 文件 用途
    SingleTurnAgentLoop "single_turn_agent" single_turn_agent_loop.py 单轮生成(默认)
    ToolAgentLoop "tool_agent" tool_agent_loop.py 多轮工具调用
  • 以 SingleTurnAgentLoop.run() 为例 (./verl/experimental/agent_loop/single_turn_agent_loop.py):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    async def run(self, sampling_params, **kwargs) -> AgentLoopOutput:
    prompt_ids = await self.apply_chat_template(messages, ...)
    output: TokenOutput = await self.server_manager.generate( # ← 调 LLMServerClient
    request_id=...,
    prompt_ids=prompt_ids,
    sampling_params=sampling_params,
    ...
    )
    return AgentLoopOutput(prompt_ids=..., response_ids=..., ...)
LLMServerClient → vLLM Replica
  • verl/workers/rollout/llm_server.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class LLMServerClient:
    async def generate(self, request_id, *, prompt_ids, sampling_params, ...):
    server_id, server = await self._acquire_server(request_id) # load_balancer 选 replica
    output = await server.generate.remote( # Ray RPC → vLLM replica
    request_id=uuid4().hex,
    prompt_ids=prompt_ids,
    sampling_params=sampling_params,
    ...
    )
    return output
    • server 是 vLLMReplica (Ray actor, GPU) 的 handle,内部 self.engine = AsyncLLM 是进程内引擎
    • 理解:verl 不做任何 Batch 处理,都是为每个请求发送(Ray RPC 调度)生成命令到 LLMServer,LLMServer 会根据收到的请求做 Continuous Batching(即连续批处理),在 LLMServer 这里实现并行调度(verl 的各个请求之间不知道对方的存在)

完整调用链路(一次 generate_sequences 调用)

  • 调用流程详细梳理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    RayPPOTrainer.fit()
    │ (Driver 进程, Python 调用)
    │
    ├─ self.async_rollout_manager.generate_sequences(combined_gen_batch)
    │ │
    │ │ [AgentLoopManager, Driver 进程内]
    │ ├─ prompts.chunk(N) → 切成 N 份
    │ ├─ asyncio.gather(*[
    │ │ worker.generate_sequences.remote(chunk) ──────┐
    │ │ ]) │ Ray RPC
    │ │ ▼
    │ │ [AgentLoopWorker #i, CPU Ray actor]
    │ │ ├─ for each sample in chunk:
    │ │ │ asyncio.create_task(
    │ │ │ self._run_agent_loop(...)
    │ │ │ )
    │ │ ├─ asyncio.gather(*tasks)
    │ │ │ │
    │ │ │ │ [每样本, 同进程内]
    │ │ │ ├─ hydra.utils.instantiate → AgentLoopBase 子类
    │ │ │ │ (SingleTurnAgentLoop / ToolAgentLoop)
    │ │ │ │
    │ │ │ ├─ agent_loop.run(sampling_params)
    │ │ │ │ │
    │ │ │ │ │ [AgentLoopBase, 同进程内]
    │ │ │ │ ├─ apply_chat_template → prompt_ids
    │ │ │ │ ├─ self.server_manager.generate(prompt_ids, ...)
    │ │ │ │ │ │
    │ │ │ │ │ │ [LLMServerClient, 同进程内]
    │ │ │ │ │ ├─ _acquire_server(request_id)
    │ │ │ │ │ │ → load_balancer.acquire_server.remote()
    │ │ │ │ │ │ → 返回 (server_id, vLLMReplica handle)
    │ │ │ │ │ ├─ server.generate.remote(prompt_ids, ...)
    │ │ │ │ │ │ │
    │ │ │ │ │ │ │ Ray RPC
    │ │ │ │ │ │ ▼
    │ │ │ │ │ │ [vLLMHttpServer, GPU Ray actor]
    │ │ │ │ │ │ ├─ self.engine.generate(prompt, sampling_params)
    │ │ │ │ │ │ │ │ engine.generate 被并发调用时,LLM Engine 会自动实现 Continuous Batching(并发推理)
    │ │ │ │ │ │ │ │ 进程内 Python 调用
    │ │ │ │ │ │ │ ▼
    │ │ │ │ │ │ │ [AsyncLLM engine]
    │ │ │ │ │ │ │ └─ 采样生成 token → RequestOutput
    │ │ │ │ │ │ │
    │ │ │ │ │ │ └─ return TokenOutput
    │ │ │ │ │ └─ return TokenOutput
    │ │ │ │ └─ return AgentLoopOutput
    │ │ │ └─ return _InternalAgentLoopOutput
    │ │ ├─ _postprocess → DataProto
    │ │ └─ return DataProto
    │ │ │
    │ ├─ DataProto.concat(outputs) ◄───────────────────────┘
    │ └─ return DataProto
    │
    └─ 继续 PPO 流程 (reward → old_log_prob → ref → value → advantage → update)
    • 注:engine.generate 被并发调用时,LLM Engine 会自动实现 Continuous Batching(并发推理),这完全由引擎消化,所以 verl 框架不需要自己再实现 Batch 处理了,只需要一个个把请求并发分发给 LLM Engine 即可(vLLM 和 sglang 均能实现)

补充:其他关键设计总结

  • 关键设计总结表
    维度 设计
    Manager 所在进程 AgentLoopManager、LLMServerManager、RewardLoopManager 都在 Driver 进程内,是普通 Python 对象
    Worker 所在进程 AgentLoopWorker(CPU Ray actor)、vLLMReplica(GPU Ray actor)、RewardLoopWorker(CPU/GPU Ray actor)都是独立 Ray actor
    LLMServerClient 的角色 轻量路由器,不持有 GPU,被 AgentLoopWorker 和 AgentLoopBase 共享持有
    AgentLoopBase 的生命周期 每样本实例化(hydra.utils.instantiate),用完即弃;_run_agent_loop 每次调用都 new 一个
    并发模型 两层并发:① Manager 层:N 个 Worker 跨 actor 并行(Ray);② Worker 层:chunk 内多个样本 asyncio 并发(单进程协程)
    CPU/GPU 分离 AgentLoopWorker 纯 CPU(编排 + tokenizer + tool 执行),vLLM replica 在 GPU 上;两者通过 Ray RPC 解耦
    reward_loop_worker_handles 的传递 可选注入;启用 streaming reward 时,AgentLoopWorker 在 agent loop 内直接调用 reward worker,实现生成与奖励计算的 Pipeline 并行
    权重同步 CheckpointEngineManager 持有 actor_rollout_wg(训练侧)和 replicas(推理侧),训练更新后通过 IPC 把权重推到 vLLM engine

VeRL NativeTool 和 FunctionTool 比较

NativeTool 与 FunctionTool 的定位差异

  • 位置:两者都通过 verl/tools/tool_registry.py 的 load_all_tools 函数加载和合并返回
  • 核心:
    • 工具需要 per-trajectory 状态、异步生命周期、自定义 step reward,或需要外部资源(沙盒、连接池)-> NativeTool
    • 工具是无状态纯函数,schema 能从签名/docstring 推断 -> FunctionTool ,配置成本显著更低
  • 定位不同:
    维度 NativeTool(BaseTool 子类) FunctionTool(@dataclass)
    状态模型 有状态,per-trajectory instance_id,提供 create/execute/calc_reward/release 全生命周期 无状态,单次 call(parameters)
    Schema 来源 YAML 显式声明,或由 get_openai_tool_schema() 方法生成 由函数签名 + Google-style docstring 自动推断(transformers.utils.get_json_schema)
    配置形态 YAML 文件,经 tool_config_path 指定 Python 文件,经 function_tool_path 指定,用 @function_tool 装饰器注册
    加载缓存 每次 load_all_tools 都重新实例化(两次初始化的对象必须不同 测试代码中有 ``) 全局 FUNCTION_TOOL_REGISTRY 缓存,同一 path 只 import 一次
    执行入口 async execute(instance_id, parameters) -> (ToolResponse, reward, metrics) async call(parameters) -> Any,再经 normalize_function_tool_return 归一化
    同步/异步 execute 恒为协程 装饰器自动检测 iscoroutinefunction,同步函数用 asyncio.to_thread 包装
    奖励信号 内置 calc_reward 钩子,可基于 tool state 产出 step reward 仅靠返回值 (response, reward, metrics) 元组携带,默认 0.0
    典型场景 沙盒执行、搜索引擎、爬虫等需要会话状态/外部资源管理的复杂工具 计算器、天气查询等无状态的工具函数
  • 配置上二者 可共存
    • 这两个工具调用的配置默认见 verl/trainer/config/rollout/rollout.yaml
      • 两个字段是可以同时配置的
    • 注意:工具名必须全局唯一
      • 对于重复的情况: tools/tool_registry.py 中的 load_all_tools 函数会检查会抛 ValueError

NativeTool 与 FunctionTool 典型配置

NativeTool,YAML 配置(tool_config_path)
  • 来自 下面 tests/tools/test_mixed_tools_on_cpu.py ,test_mixed_tools_on_cpu.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    tools:
    - class_name: "tests.tools._stub_search_tools.StubSearchTool" # BaseTool 子类全限定名
    config:
    retrieval_service_url: http://stub/retrieve # 业务参数
    topk: 3
    type: native # ToolType.NATIVE
    tool_schema: # OpenAI function schema 显式声明
    type: function
    function:
    name: search
    description: Stub web search.
    parameters:
    type: object
    properties:
    query_list:
    type: array
    description: A list of fully-formed semantic queries.
    required: ["query_list"]
  • 对应的 BaseTool 子类实现见 examples/tutorial/agent_loop_get_started/sandbox.py,需重写 execute(必要时还有 create/release/calc_reward)

FunctionTool,Python 文件(function_tool_path)

  • 来自 tests/experimental/agent_loop/function_tool_examples.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from verl.tools.function_tool import function_tool

    @function_tool # name 默认取函数名;也可 @function_tool("custom_name")
    def get_weather(city: str) -> dict:
    """Get the current weather for a city.

    Args:
    city: The city to look up, e.g. "Tokyo" or "San Francisco".
    """
    return {"temperature_c": 17.3, "condition": "drizzle"}
  • @function_tool 装饰器的注意事项:

    • @function_tool(verl/tools/function_tool.py)装饰器如果没有传入 custom_name 作为参数,则装饰内部会通过 _build_schema_from_fn 调 get_json_schema(fn) 自动推断 schema

    • 自动推断 schema 时的函数需要满足以下条件:

      • 必须带 Google-style docstring + 参数类型注解(注:这个要求来自最底层的 transformers.utils.get_json_schema(fn) 给的约束)

        • 具体示例如下:

          1
          2
          3
          4
          5
          6
          7
          8
          9
          @function_tool
          def get_weather(city: str) -> dict: # 参数类型注解
          """Get the current weather for a city. # description

          # 下面是 Google-style 参数定义:
          Args:
          city: The city to look up, e.g. "Tokyo".
          """
          ...
        • get_json_schema(get_weather) 解析结果:

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          24
          25
          26
          27
          28
          29
          30
          31
          32
          33
          34
          35
          36
          37
          38
          39
          40
          41
          42
          43
          44
          45
          46
          47
          48
                          {
          "type": "function",
          "function": {
          "name": "get_weather",
          "description": "Get the current weather for a city. # description\n\n# 下面是 Google-style 参数定义:",
          "parameters": {
          "type": "object",
          "properties": {
          "city": {
          "type": "string",
          "description": "The city to look up, e.g. \"Tokyo\"."
          }
          },
          "required": [
          "city"
          ]
          },
          "return": {
          "type": "object"
          }
          }
          }
          ```
          * `FunctionTool.call` 的返回结果总是会被 `verl/tools/function_tool.py` 中的 `normalize_function_tool_return` 函数 统一为 `(ToolResponse, reward, metrics)`
          * `FunctionTool.call` (即 `fn`)的返回结果可以是 ToolResponse, str, dict, tuple 或其他任意可以转换成 str 的类型
          * 其实任何对象理论上都可以转换成 str,所以返回值可以是任意类型
          * 如果是 tuple 时,必须返回值在 1-3 个之间,不能是其他数字,对应 `response, reward, metrics`(一定会依次转换成 `(ToolResponse, float, dic)` 类型)
          * `verl/tools/function_tool.py` 中的 `normalize_function_tool_return` 函数定义
          ```python
          def normalize_function_tool_return(ret: Any) -> tuple[ToolResponse, float, dict]:
          if isinstance(ret, ToolResponse): # 如果结果已经是 ToolResponse,直接返回 ToolResponse, reward 0.0, metrics {}
          return ret, 0.0, {}
          if isinstance(ret, str): # 如果结果是字符串,将字符串转换为 ToolResponse, reward 0.0, metrics {}
          return ToolResponse(text=ret), 0.0, {}
          if isinstance(ret, dict): # 如果结果是字典,将字典转换为 ToolResponse, reward 0.0, metrics {}
          return ToolResponse(text=json.dumps(ret, ensure_ascii=False)), 0.0, {}
          if isinstance(ret, tuple): # 如果结果是元组,根据元组长度判断是否包含 reward 和 metrics
          if not 1 <= len(ret) <= 3:
          raise TypeError(
          f"@function_tool return tuple must have length 1, 2, or 3 "
          f"(got length {len(ret)}: {ret!r}). Use (response,), "
          f"(response, reward), or (response, reward, metrics)."
          )
          response = _coerce_response(ret[0])
          reward = 0.0 if len(ret) < 2 or ret[1] is None else float(ret[1]) # 转换成 float 类型
          metrics = {} if len(ret) < 3 or ret[2] is None else dict(ret[2]) # 转换成字典
          return response, reward, metrics
          return ToolResponse(text=str(ret)), 0.0, {} # 将其他类型的结果转换为 ToolResponse, reward 0.0, metrics {}

NLP——Chat-Template使用说明

关键词:chat_template, chat_chat_template, chat template

  • 参考链接:
    • 【官方】tokenizer 使用说明

整体说明

  • Chat Template 是为了适配不同模型产生的
  • 理论上 Chat Template 与模型一一绑定,但同系列的模型经常相同
  • Chat Template 定义一般在 tokenizer_config.json 文件中的 chat_template 字段
  • 使用 Chat Template 的函数是 AutoTokenizer.apply_chat_template,这是Hugging Face Transformers 库中 tokenizer 的一个核心方法
  • 论文主要详细介绍 apply_chat_template 函数的用法

apply_chat_template 函数签名

  • 函数签名详情:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    def apply_chat_template(
    self,
    conversation: Union[list[dict[str, str]], list[list[dict[str, str]]]],
    tools: Optional[list[Union[dict, Callable]]] = None,
    documents: Optional[list[dict[str, str]]] = None,
    chat_template: Optional[str] = None,
    add_generation_prompt: bool = False,
    continue_final_message: bool = False,
    tokenize: bool = True,
    padding: Union[bool, str, PaddingStrategy] = False,
    truncation: bool = False,
    max_length: Optional[int] = None,
    return_tensors: Optional[Union[str, TensorType]] = None,
    return_dict: bool = False,
    return_assistant_tokens_mask: bool = False,
    tokenizer_kwargs: Optional[dict[str, Any]] = None,
    **kwargs,
    ) -> Union[str, list[int], list[str], list[list[int]], BatchEncoding]:

apply_chat_template 核心参数详解

conversation(必需参数)

  • 对话数据,可以是单个对话列表或批量对话列表,每个消息必须包含 "role" 和 "content" 键
  • 参数类型: Union[list[dict[str, str]], list[list[dict[str, str]]]]
  • 简单参考示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 单轮对话
    conversation = [
    {"role": "user", "content": "你好,请问今天天气如何?"},
    {"role": "assistant", "content": "您好!我需要知道您的位置才能提供准确的天气信息。"}
    ]

    # 多轮对话
    conversation = [
    {"role": "system", "content": "你是一个有用的AI助手"},
    {"role": "user", "content": "解释量子计算"},
    {"role": "assistant", "content": "量子计算是利用量子力学现象进行计算的技术..."},
    {"role": "user", "content": "它与传统计算有什么区别?"}
    ]

tokenize

  • 控制输出格式是否为 tokenized(token ID 列表)还是文本字符串
  • 参数类型: bool,默认值是 True
    • 对于需要直接传递给模型的场景,建议保持 True;
    • 对于需要查看格式化文本或进行自定义处理的场景,可以设置为 False
  • 简单参考示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 输出文本格式
    text_output = tokenizer.apply_chat_template(
    conversation,
    tokenize=False
    )
    print(text_output) # 例如: <|user|>你好!<|end|><|assistant|>

    # 输出token IDs
    token_output = tokenizer.apply_chat_template(
    conversation,
    tokenize=True
    )

add_generation_prompt

  • 是否在格式化输出末尾添加助手回复的提示符
  • 参数类型: bool,**默认值: False
  • 这是确保模型正确生成助手回复的关键参数
    • 当设置为 True 时,会在对话末尾添加表示助手开始回复的特殊标记
  • 简单参考示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # 不添加生成提示
    output_without = tokenizer.apply_chat_template(
    conversation,
    add_generation_prompt=False,
    tokenize=False
    )
    # 输出: <|user|>你好!<|end|>

    # 添加生成提示
    output_with = tokenizer.apply_chat_template(
    conversation,
    add_generation_prompt=True,
    tokenize=False
    )
    # 输出: <|user|>你好!<|end|><|assistant|>

continue_final_message

  • 是否继续最后一个消息而不是开始新消息
  • 参数类型: bool,默认值是 False
  • 用于”预填充”模型回复的场景,当你想让模型继续已有的助手消息时使用
    • 不能与 add_generation_prompt 同时使用
  • 简单参考示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 预填充助手回复
    conversation = [
    {"role": "user", "content": "请用JSON格式回答"},
    {"role": "assistant", "content": '{"answer": "'}
    ]

    # 模型将继续这个JSON格式,而不是开始新消息
    formatted = tokenizer.apply_chat_template(
    conversation,
    continue_final_message=True,
    tokenize=True
    )
    ## continue_final_message=True,此时不拼接结束符号
    # [Round 0] USER:请用JSON格式回答 ASSISTANT:{"answer": "
    ## continue_final_message=False(默认),此时拼接结束符号,表示本次会话已经结束
    # [Round 0] USER:请用JSON格式回答 ASSISTANT:{"answer": "</longcat_s>

chat_template

  • 自定义的 Jinja2 模板字符串
  • 参数类型: Optional[str],默认值为 None
  • 当需要使用非默认模板或测试新模板格式时使用
  • 简单参考示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    custom_template = """
    {% for message in messages %}
    {% if message['role'] == 'user' %}
    Human: {{ message['content'] }}
    {% elif message['role'] == 'assistant' %}
    Assistant: {{ message['content'] }}
    {% endif %}
    {% endfor %}
    """

    output = tokenizer.apply_chat_template(
    conversation,
    chat_template=custom_template,
    tokenize=False
    )

tools

  • 工具列表,用于函数调用场景

  • 参数类型: Optional[list[Union[dict, Callable]]],默认值为 None

  • 每个工具应该是 JSON Schema 格式,包含名称、描述和参数类型

  • 简单参考示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    tools = [
    {
    "type": "function",
    "function": {
    "name": "get_weather",
    "description": "获取天气信息",
    "parameters": {
    "type": "object",
    "properties": {
    "location": {"type": "string", "description": "城市名称"}
    },
    "required": ["location"]
    }
    }
    }
    ]

    formatted = tokenizer.apply_chat_template(
    conversation,
    tools=tools,
    add_generation_prompt=True
    )
    • 以上函数的调用方式如 get_weather(**call_info["arguments"])
      • 其中 call_info["arguments"] 是一个必须包含 "location" 的字典

documents(待尝试)

  • 文档列表,用于 RAG(检索增强生成)场景
  • 参数类型: Optional[list[dict[str, str]]],默认值为 None
    • 推荐格式为 每个文档应包含 "title" 和 "text" 键
  • 简单参考示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    documents = [
    {
    "title": "量子计算简介",
    "text": "量子计算是利用量子力学现象进行计算的技术..."
    },
    {
    "title": "量子比特",
    "text": "量子比特是量子计算的基本单位..."
    }
    ]

    formatted = tokenizer.apply_chat_template(
    conversation,
    documents=documents,
    add_generation_prompt=True
    )

多模态 chat-template 使用详解

  • 注:多模态中,Processor 组合了 Tokenizer + 图像处理
    • 理论上,Processor 中已经包含了处理文本的 Tokenizer 了

简单示例:多模态下的模型生成示例(可本地执行)

  • 模型生成示例(MacOS 也可执行)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    # 阻止 fla 被 transformers 动态导入(For MacOS,平时不需要这部分)
    import sys
    sys.modules["fla"] = None
    try:
    import fla
    except ImportError:
    pass

    from transformers import AutoModelForCausalLM, AutoProcessor
    from qwen_vl_utils import process_vision_info
    from PIL import Image

    # 1. 加载模型和 Processor
    model_path = "/Users/sanye/llm/model/Qwen3.5-0.8B"
    processor = AutoProcessor.from_pretrained(model_path)
    # 加载到 CPU 中执行 (For MacOS)
    model = AutoModelForCausalLM.from_pretrained(model_path, device_map="cpu", torch_dtype="float32")

    # 2. 准备对话消息
    messages = [
    {
    "role": "user",
    "content": [
    # 传入本地 image 对象
    {"type": "image", "image": "/Users/sanye/llm/image/candy.JPG"},
    {"type": "text", "text": "What animal is on the candy?"}
    ]
    },
    ]

    # 3. 预处理视觉信息 (关键步骤)
    # 这一步会读取并处理图片,返回一个 PIL.Image 对象的列表
    image_inputs, video_inputs = process_vision_info(messages)

    # 4. 处理文本和视觉信息
    # 首先,使用 apply_chat_template 生成纯文本部分
    text = processor.apply_chat_template(
    messages,
    tokenize=False, # 先不 tokenize,只生成文本字符串
    add_generation_prompt=True,
    )

    # 然后,将文本和图像一起传给 processor 进行最终的 tokenization 和处理
    inputs = processor(
    text=[text],
    images=image_inputs,
    videos=video_inputs, # 如果没有视频,传入 None 即可
    padding=True,
    return_tensors="pt",
    ).to(model.device)

    print(inputs)

    # 5. 生成输出
    # 至此,inputs 可以直接作为 generate 的参数使用
    outputs = model.generate(**inputs, max_new_tokens=40)
    # 解码并打印生成的文本
    print(processor.decode(outputs[0][inputs["input_ids"].shape[-1]:], skip_special_tokens=True))
    # # 输出类似下面的形式:
    # Based on the image, there are **two animals** painted on the candies:
    #
    # 1. **A turtle** — painted on the teal candy in the upper part of the hand.
    • qwen_vl_utils.process_vision_info 的详细说明见本文附录
    • 所有函数的更多详细说明见下文

构造简单示例和详细解读

  • 先生成可读 chat_template 模版,再进行 tokenize 的示例(简单图片示例)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    from transformers import AutoProcessor
    model_path = "/Users/sanye/llm/model/Qwen3.5-0.8B"
    processor = AutoProcessor.from_pretrained(model_path)
    # 读取本地图片
    from PIL import Image
    # image = Image.open("/Users/sanye/llm/image/candy.JPG").convert("RGB")
    import numpy as np
    # 随机初始化一张 (256 x 256 x 3) 的图片,256 = 16 * 16,16 是 Qwen3.5 patch 像素切割单位(config.json 中的 patch_size), 16 是默认最小 patch 数(小于 16 也会填充到 16)
    image = Image.fromarray(np.random.randint(0, 255, (256, 256, 3), dtype=np.uint8))

    messages = [
    {
    "role": "user",
    "content": [
    {"type": "text", "text": "We have two images"},
    {"type": "text", "text": "\n# image1:"},
    {"type": "image", "image": image},
    {"type": "text", "text": "\n# image2:"},
    {"type": "image", "image": image},
    {"type": "text", "text": "\nWhat animal is on the candy?"},
    ]
    },
    ]
    prompt = processor.apply_chat_template(
    messages,
    add_generation_prompt=True,
    tokenize=False
    )
    # 输出结果见末尾
    print(prompt)

    from qwen_vl_utils import process_vision_info
    # 预处理视觉信息,这一步会下载并处理图片和视频(若存在),返回 PIL.Image 对象的列表
    images, videos = process_vision_info(messages)
    # 等价于:images = [image, image]
    inputs = processor(
    text=prompt,
    images=images,
    return_tensors="pt"
    )

    # 下面得到的结果和前面的示例一模一样
    print(inputs)
    for key in inputs:
    value = inputs[key]
    print(f"key: {key}")
    print(f"value: {value.shape}")

    # <|im_start|>user
    # We have two images
    # # image1:<|vision_start|><|image_pad|><|vision_end|>
    # # image2:<|vision_start|><|image_pad|><|vision_end|>
    # What animal is on the candy?<|im_end|>
    # <|im_start|>assistant
    # <think>
    #
    # </think>
    #
    #
    # {'input_ids': tensor([[248045, 846, 198, 1596, 599, 1330, 5167, 198, 2,
    # 2099, 16, 25, 248053, 248056, 248056, 248056, 248056, 248056,
    # 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056,
    # 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056,
    # 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056,
    # 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056,
    # 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056,
    # 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056,
    # 248056, 248056, 248056, 248056, 248056, 248054, 198, 2, 2099,
    # 17, 25, 248053, 248056, 248056, 248056, 248056, 248056, 248056,
    # 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056,
    # 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056,
    # 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056,
    # 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056,
    # 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056,
    # 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056, 248056,
    # 248056, 248056, 248056, 248056, 248054, 198, 3710, 9572, 369,
    # 383, 279, 30517, 30, 248046, 198, 248045, 74455, 198,
    # 248068, 271, 248069, 271]]),
    # '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,
    # 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    # 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    # 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    # 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    # 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    # 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]),
    # 'mm_token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    # 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    # 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    # 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    # 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    # 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    # 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]),
    # 'pixel_values': tensor([[ 0.9059, 0.8980, 0.8745, ..., -0.7725, -0.7725, -0.7333],
    # [ 0.0431, 0.1765, 0.3176, ..., 0.4353, 0.4667, 0.4824],
    # [ 0.0745, 0.0667, 0.0588, ..., 0.4667, 0.3961, 0.3098],
    # ...,
    # [ 0.1451, 0.1373, 0.1137, ..., -0.7412, -0.7569, -0.7647],
    # [-0.4431, -0.4902, -0.5294, ..., 0.2000, -0.0745, -0.3255],
    # [-0.6157, -0.5843, -0.5686, ..., 0.5373, 0.5686, 0.5843]]),
    # 'image_grid_thw': tensor([[ 1, 16, 16],
    # [ 1, 16, 16]])}
    #
    # key: input_ids
    # value: torch.Size([1, 166])
    # key: attention_mask
    # value: torch.Size([1, 166])
    # key: mm_token_type_ids
    # value: torch.Size([1, 166])
    # key: pixel_values
    # value: torch.Size([512, 1536])
    # key: image_grid_thw
    # value: torch.Size([2, 3])
  • processor.apply_chat_template 说明:

    • 这里可视化的结果中,将每一张图片都作为一个 placeholder ( Qwen3.5 中是 <|vision_start|><|image_pad|><|vision_end|>)专门存放起来了
      • 整个输出的 prompt 是可读的
    • 后续在进行 tokenize 时,需要传入这个 Prompt 和 image 信息(对齐顺序),从而保证 tokenize 后将图片 Token 正确插入到指定的顺序中
  • 说明:inputs 对象包含了处理多模态输入(文本 + 图像)后得到的张量,各字段含义如下:

    • input_ids :

      • Shape: (1, seq_len)
        • 示例中 seq_len = 166
      • 经过 tokenizer 编码后的完整提示文本(包含特殊标记如 <|vision_start|>(248053)、<|image_pad|>(248056) 、<|vision_end|>(248054)等)的 token ID 序列
      • 这里的长度和 Prompt 是完全对不齐的,多出来的 Token 就是图片转换成的 Token 占位符
        • 注意:这里的图片 Token 占位符的 连续 64 个 Token 都是 248056 (<|image_pad|>),本质上不包含任何数据
        • 注意:这里的 Token 数和后面对应的 patch 数并不相等(详情见 image_grid_thw 中 每张图片被 ViT 编码成 256 个 Token), 因为还会压缩合并 Token
          • 通过 空间合并(Spacial Merging)技术,patch 经过压缩后得到的数字才是结果
            • 通过模型结构中的 PatchMerger 模块(一般是 MLP)来合并相邻的 多个 (n x n) Token 为一个 Token,一般都选择 spatial_merge_size=2 (即长宽各合并 2 个 Token 合并(2x2)为一组)
          • 比如这里经过了 spatial_merge_size=2,最终得到 256/(2x2)=64 个 最终的 Token
          • 由于目前大部分模型(比如 Kimi K2.5 等)都不需要生成 图片或视频,所以不涉及到解码(NTP 训练时,视觉 Token 对应的位置不会有损失回传)
          • 最后在模型中真正参与 Attention 的是这些 Merging 后的 Token,但是为了保证 Patch 位置可识别,会在 2D-NoPE 上添加补偿
    • attention_mask :

      • Shape: 与 input_ids 相同
      • 标记每个位置是否为有效 token(1)或填充(0),用于自注意力机制中忽略填充部分
    • mm_token_type_ids :

      • Shape: 与 input_ids 相同
      • 用于区分 token 的类型,例如文本 token 和视觉 token(图像占位符),方便模型在多模态处理时识别不同模态的输入
    • pixel_values (ViT 编码后的结果) :

      • Shape:(total_patches, feature_dim)
        • 在本例中为 (512, 1536)
      • 它并不是原始的像素矩阵,而是 所有图像经过视觉编码器(如 ViT)分割成 patch 并投影后的特征向量 ,按顺序拼接在一起
      • total_patches = 512 等于所有图像的 patch 总数 ,feature_dim = 1536 是每个 patch 的 Embedding 维度
      • 如何区分不同图片 :
        • pixel_values 本身只是线性拼接,不直接携带图片归属信息
        • 需要结合 image_grid_thw 来确定每张图片的 patch 数量
          • 理解:
            • pixel_values 将多张图片的 patch 特征按传入顺序拼接为单张二维张量(单从这里无法切分多张图片的内容)
            • image_grid_thw 记录了每张图片的网格结构,通过计算每张图片的 patch 总数,即可从 pixel_values 中切分出每张图片的特征,从而区分不同图片
    • image_grid_thw (ViT 编码 Token 的信息):

      • Shape (num_images, 3)
        • 本例为 (2, 3),表示有 num_images=2 张图片
      • 每张图片的 3 维时空网格尺寸 (temporal, height, width),本示例中每张图片的值为 [1, 16, 16]
        • temporal 是时间维度
          • 对于静态图像,temporal 通常为 1
          • 对于视频,是根据 temporal ≈ num_frames / temporal_patch_size 来确定
            • num_frames:模型实际采样并处理的视频帧数,模型不会处理视频的每一帧,而是会进行采样
            • temporal_patch_size:时序上的“合并”尺寸
              • Qwen2.5-VL默认将连续的 2 帧合并为 1 个“时序补丁”(temporal patch)
                • 即 temporal_patch_size=2,配置在 config.json 中
              • 这是实现时序维度的 PatchMerger
        • height 和 width 是图像被划分的 patch 网格数(例如本例中为 16x16)
        • 注意:这里已经没有了传统图片的 Channel 维度,在编码为 Patch 时,图片的 channel 维度信息已经被编码到 1536 维度的向量中了
      • 每张图片的 patch 总数 = temporal × height × width
        • 本例中每张图片有 1 × 16 × 16 = 256 个 patch,因此 pixel_values 的前 256 行对应第一张图片,后 256 行对应第二张图片(有序索引)
  • 补充:更多真实图片下的示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    from transformers import AutoProcessor
    model_path = "/Users/sanye/llm/model/Qwen3.5-0.8B"
    processor = AutoProcessor.from_pretrained(model_path)
    # 读取本地图片
    from PIL import Image
    image = Image.open("/Users/sanye/llm/image/candy.JPG").convert("RGB")

    messages = [
    {
    "role": "user",
    "content": [
    {"type": "text", "text": "We have two images"},
    {"type": "text", "text": "\n# image1:"},
    {"type": "image", "image": image},
    {"type": "text", "text": "\n# image2:"},
    {"type": "image", "image": image},
    {"type": "text", "text": "\nWhat animal is on the candy?"},
    ]
    },
    ]
    prompt = processor.apply_chat_template(
    messages,
    add_generation_prompt=True,
    tokenize=False
    )
    # 输出结果见末尾
    print(prompt)


    from qwen_vl_utils import process_vision_info
    # 预处理视觉信息,这一步会下载并处理图片和视频(若存在),返回 PIL.Image 对象的列表
    images, videos = process_vision_info(messages)

    inputs = processor(
    text=prompt,
    images=images,
    return_tensors="pt"
    )

    # 下面得到的结果和前面的示例一模一样
    print(inputs)
    for key in inputs:
    value = inputs[key]
    print(f"key: {key}")
    print(f"value: {value.shape}")

    # # print(prompt) 输出如下:
    # <|im_start|>user
    # We have two images
    # # image1:<|vision_start|><|image_pad|><|vision_end|>
    # # image2:<|vision_start|><|image_pad|><|vision_end|>
    # What animal is on the candy?<|im_end|>
    # <|im_start|>assistant
    # <think>
    #
    # </think>
    #
    # # print(inputs) 输出如下:
    # {'input_ids': tensor([[248045, 846, 198, ..., 271, 248069, 271]]),
    # 'attention_mask': tensor([[1, 1, 1, ..., 1, 1, 1]]),
    # 'mm_token_type_ids': tensor([[0, 0, 0, ..., 0, 0, 0]]),
    # 'pixel_values': tensor([[ 0.4588, 0.5059, 0.5294, ..., 0.5373, 0.5608, 0.5686],
    # [ 0.5373, 0.5137, 0.5137, ..., 0.5216, 0.5451, 0.5216],
    # [ 0.5451, 0.5451, 0.5451, ..., 0.5922, 0.5686, 0.5608],
    # ...,
    # [-0.3020, -0.2549, -0.2706, ..., -0.1765, -0.1686, -0.2000],
    # [-0.1922, -0.1451, -0.1216, ..., -0.0275, -0.1059, -0.0980],
    # [-0.0510, -0.0510, -0.0588, ..., -0.1059, -0.1059, -0.1216]]),
    # 'image_grid_thw': tensor([[ 1, 188, 252],
    # [ 1, 188, 252]])}
    #
    # key: input_ids
    # value: torch.Size([1, 23726])
    # key: attention_mask
    # value: torch.Size([1, 23726])
    # key: mm_token_type_ids
    # value: torch.Size([1, 23726])
    # key: pixel_values
    # value: torch.Size([94752, 1536])
    # key: image_grid_thw
    # value: torch.Size([2, 3])
    • 解读:
      • pixel_values :
        • Shape:(total_patches, feature_dim)
          • 在本例中为 (94752, 1536)
        • 它并不是原始的像素矩阵,而是所有图像经过视觉编码器(如 ViT)分割成 patch 并投影后的特征向量 ,按顺序拼接在一起
        • total_patches = 94752 等于所有图像的 patch 总数 ,feature_dim = 1536 是每个 patch 的 Embedding 维度
        • 如何区分不同图片 :
          • pixel_values 本身只是线性拼接,不直接携带图片归属信息
          • 需要结合 image_grid_thw 来确定每张图片的 patch 数量
      • image_grid_thw :
        • Shape (num_images, 3)
          • 本例为 (2, 3),表示有 num_images=2 张图片
        • 每张图片的 3 维时空网格尺寸 (temporal, height, width),本示例中每张图片的值为 [1, 188, 252]
          • 对于静态图像,temporal 通常为 1
          • height 和 width 是图像被划分的 patch 网格数(例如本例中为 188×252)
        • 每张图片的 patch 总数 = temporal × height × width
          • 本例中每张图片有 1 × 188 × 252 = 47376 个 patch,因此 pixel_values 的前 47376 行对应第一张图片,后 47376 行对应第二张图片(有序索引)

更多合并的编码示例(注:不建议合并使用)

  • 注:建议先 apply_chat_template 为可读的文本,然后再进行 Tokenize 更合适
  • 合并编码(仅调用一次 processor,看似更简单,但是不推荐)情况展示
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    from transformers import AutoProcessor
    model_path = "/Users/sanye/llm/model/Qwen3.5-0.8B"
    processor = AutoProcessor.from_pretrained(model_path)
    messages = [
    {
    "role": "user",
    "content": [
    {"type": "image", "image": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"},
    {"type": "image", "image": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"},
    {"type": "text", "text": "What animal is on the candy?"}
    ]
    },
    ]
    inputs = processor.apply_chat_template(
    messages,
    add_generation_prompt=True,
    tokenize=True,
    return_dict=True,
    return_tensors="pt",
    )
    print(inputs)
    for key in inputs:
    value = inputs[key]
    print(f"key: {key}")
    print(f"value: {value.shape}")

    # {'input_ids': tensor([[248045, 846, 198, ..., 271, 248069, 271]]),
    # 'attention_mask': tensor([[1, 1, 1, ..., 1, 1, 1]]),
    # 'mm_token_type_ids': tensor([[0, 0, 0, ..., 0, 0, 0]]),
    # 'pixel_values': tensor([[ 0.4588, 0.5059, 0.5294, ..., 0.5373, 0.5608, 0.5686],
    # [ 0.5373, 0.5137, 0.5137, ..., 0.5216, 0.5451, 0.5216],
    # [ 0.5451, 0.5451, 0.5451, ..., 0.5922, 0.5686, 0.5608],
    # ...,
    # [-0.3020, -0.2549, -0.2706, ..., -0.1765, -0.1686, -0.2000],
    # [-0.1922, -0.1451, -0.1216, ..., -0.0275, -0.1059, -0.0980],
    # [-0.0510, -0.0510, -0.0588, ..., -0.1059, -0.1059, -0.1216]]),
    # 'image_grid_thw': tensor([[ 1, 188, 252],
    # [ 1, 188, 252]])}
    #
    # key: input_ids
    # value: torch.Size([1, 23711])
    # key: attention_mask
    # value: torch.Size([1, 23711])
    # key: mm_token_type_ids
    # value: torch.Size([1, 23711])
    # key: pixel_values
    # value: torch.Size([94752, 1536])
    # key: image_grid_thw
    # value: torch.Size([2, 3])

输出控制参数

return_tensors

  • 指定返回的张量类型,return_tensors 参数仅在 tokenize=True 时生效
  • 为了同时支持多种框架,特意设计了不同的返回值类型,减少一次使用时重新转类型的时间,提升速度
  • 参数类型: Optional[Union[str, TensorType]]
    • 默认值为 None,此时返回 原生 Python 数据结构
    • 可选值: 'pt' (PyTorch), 'tf' (TensorFlow), 'np' (NumPy), 'jax' (JAX)
  • 返回张量类型详细说明:
    • 若 tokenize=False,则无论如何返回值都是原生 Python 数据结构
    • 若 tokenize=True,则根据 return_tensors 判断返回类型
      • 默认值为 None,此时返回 原生 Python 数据结构
      • 指定时根据上述指定类型返回值
  • 简单参考示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # PyTorch tensors
    pytorch_output = tokenizer.apply_chat_template(
    conversation,
    return_tensors="pt",
    add_generation_prompt=True
    )

    # NumPy arrays
    numpy_output = tokenizer.apply_chat_template(
    conversation,
    return_tensors="np",
    add_generation_prompt=True
    )

return_dict

  • 是否返回包含多个字段的字典,仅在 tokenize=True 时有效
  • 参数类型:bool,默认值为 False
  • 当 apply_chat_template 函数的 return_dict 参数设为 True 时,函数会返回一个字典格式的结果 ,而非默认的张量(或字符串)
    • 默认(return_dict=False):若 tokenize=True,直接返回 input_ids 张量(若开启 padding,会返回形状为 [batch_size, seq_len] 的张量)
    • return_dict=True:返回字典,键值对应模型输入的核心要素,结构清晰,无需手动区分张量类型
  • 字典中会根据需要明确包含 input_ids、attention_mask 等模型输入所需的关键组件,便于直接拆解和使用
    • 当 return_assistant_tokens_mask=True 时,会多返回一个 assistant_masks 字段
  • 具体来说,返回的 output 是一个字典,包含以下关键键(根据参数配置可能增减):
    • "input_ids":对话文本对应的token ID张量(模型输入核心)
    • "attention_mask":注意力掩码张量(标记哪些token是有效文本,哪些是padding,避免模型关注padding)

return_assistant_tokens_mask

  • 参数类型: bool,默认值为 False

  • 当 return_assistant_tokens_mask=True 时,会多返回一个 assistant_masks 字段

  • 是否返回助手生成 token 的掩码,助手生成的 token 对应掩码为 1,用户和系统 token 对应掩码为 0

    • 只在 tokenize=True 且 return_dict=True 时生效,否则返回错误:
      1
      ValueError: `return_dict=True` is incompatible with `tokenize=False`, because there is no dict of tokenizer outputs to return.
  • 仅支持包含 {% generation %} ... {% endgeneration %} 关键字的聊天模板

    • 这个关键字不会影响正常的渲染,只是用于标记 assistant 的内容
    • 一般来说,用该标记将 assistant 的整个内容(包括应该学习的所有内容,如 tools 调用等内容都包括进来)
    • 注意:原始的 Jinja 语法是不可以随便写这种位置标签的,但是 apply_chat_template 会对 这个标签做特殊处理,所以不用担心
      • 验证:若随机增加未知的标签 {% generationa %} ... {% endgenerationa %} 则会出现下面的问题:
        1
        jinja2.exceptions.TemplateSyntaxError: Encountered unknown tag 'generationa'. Jinja was looking for the following tags: 'elif' or 'else' or 'endif'. The innermost block that needs to be closed is 'if'.
  • 亲测:return_assistant_tokens_mask=True 但对 chat_template 有一定的要求(要求包含 {% generation %} 用于标记 assistant 位置),当前大部分 chat_template 都不支持,此时会全屏蔽(返回 assistant_masks 字段全是 0)

    • 警告信息如下:
      1
      return_assistant_tokens_mask==True but chat template does not contain `{% raw %} {% generation %} {% endraw %}` keyword.
  • 常用用途:用于训练或分析,标识哪些 token 是由助手生成的

  • 简单参考示例:

    1
    2
    3
    4
    5
    6
    7
    8
    output = tokenizer.apply_chat_template(
    conversation,
    return_dict=True,
    tokenize=True,
    return_assistant_tokens_mask=True,
    add_generation_prompt=True
    )
    # 输出包含助手token掩码信息
    • 返回结果 output 多多包涵一个 assistant_masks 的 list 类型字段,里面为 1 的地方都是 assistant 的 Token
    • 需要注意返回结果中为 1 的 Token 中,可能会多出来一些自定义的 Token,此时需要手动处理一下
      • 比如在 assistant 信息前面加入的 <USER> Token 通常会被包含
      • 此时需要自己手动识别并去除一下相关的特殊 Token

序列处理参数

padding

  • 用于指定填充类型
  • 参数类型: Union[bool, str, PaddingStrategy],默认值为 False
  • 仅在 tokenize=True 时生效
    • 虽然说明文档中未明确说明这一点,但经过测试,tokenize=False 时不会 padding
  • 可选值为
    • True 或 'longest': 填充到批次中最长序列
    • 'max_length': 填充到指定最大长度,最大长度由 max_length 参数指定
    • False(默认值) 或 'do_not_pad': 不填充
  • 简单参考示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 填充到最长序列
    padded_output = tokenizer.apply_chat_template(
    batch_conversations,
    padding=True,
    tokenize=True,
    return_tensors="pt"
    )

    # 填充到最大长度
    max_length_output = tokenizer.apply_chat_template(
    conversation,
    padding="max_length",
    max_length=512,
    tokenize=True,
    return_tensors="pt"
    )
补充:padding_side 属性指定左填充 or 右填充
  • Tokenizer.apply_chat_template 方法本身并不直接提供选择左 padding(Left Padding) 或右 padding (Right Padding) 的参数
  • padding 方式通常是由 tokenizer 的整体配置(特别是 padding_side 参数)决定的,而不是由 apply_chat_template 方法单独控制
    • 如果需要设置 padding 方向,应该在初始化 tokenizer 时或通过 tokenizer.padding_side 属性进行配置
    • padding_side 可选值为 "right"(默认值) 或 "left"
  • 注:apply_chat_template 方法主要用于将对话历史格式化为模型期望的输入格式,它会调用 tokenizer 的编码逻辑,而编码过程会遵循 tokenizer 已设置的 padding_side 配置
  • 示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    from transformers import AutoTokenizer

    # 初始化 tokenizer 时指定 padding 方向
    tokenizer = AutoTokenizer.from_pretrained("model_name", padding_side="left")

    # 核心代码
    tokenizer.padding_side = "right"

    # 应用对话模板时会遵循上述 padding 配置
    chat = [
    {"role": "user", "content": "Hello!"},
    {"role": "assistant", "content": "Hi there!"}
    ]
    inputs = tokenizer.apply_chat_template(chat, tokenize=True, return_tensors="pt", padding=True)

truncation

  • 是否截断超过最大长度的序列
    • 在处理长对话时启用,避免超出模型最大长度限制
  • 参数类型: bool,默认值为 False
补充:truncation_side 属性指定左截断 or 右截断
  • 用法与 padding_side 参数类似
  • truncation_side 是 Tokenizer 类的一个属性,其可选值为:
    • "left":从序列的左侧(开头)截断
    • "right"(默认值):从序列的右侧(结尾)截断(默认值)

max_length

  • 最大长度限制(以token数计),与 padding 或 truncation 配合使用
  • 参数类型: Optional[int],默认值为 None

tokenizer_kwargs

  • 传递给分词器的额外参数,类型为 Optional[dict[str, Any]]

**kwargs

  • 传递给模板渲染器的额外参数,可在聊天模板中访问

附录:完整使用示例

基础对话生成

  • 简单对话简单示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    from transformers import AutoTokenizer, AutoModelForCausalLM
    import torch

    # 加载模型和tokenizer
    model_id = "xxx/xxx"
    tokenizer = AutoTokenizer.from_pretrained(model_id)
    model = AutoModelForCausalLM.from_pretrained(model_id, device_map="auto")

    # 准备对话
    messages = [
    {"role": "system", "content": "你是一个友好的AI助手"},
    {"role": "user", "content": "请解释机器学习的基本概念"},
    ]

    # 格式化对话
    tokenized_chat = tokenizer.apply_chat_template(
    messages,
    tokenize=True,
    add_generation_prompt=True,
    return_tensors="pt"
    )

    # 生成回复
    with torch.no_grad():
    outputs = model.generate(
    tokenized_chat,
    max_new_tokens=256,
    temperature=0.7,
    do_sample=True
    )

    # 解码回复
    response = tokenizer.decode(outputs[0], skip_special_tokens=True)
    print(response)

批量处理

  • 多个对话同时处理
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    # 批量对话
    batch_conversations = [
    [
    {"role": "user", "content": "什么是人工智能?"}
    ],
    [
    {"role": "user", "content": "如何学习编程?"}
    ]
    ]

    # 批量格式化
    batch_output = tokenizer.apply_chat_template(
    batch_conversations,
    padding=True,
    truncation=True,
    max_length=512,
    return_tensors="pt",
    add_generation_prompt=True
    )

    # 批量生成
    batch_outputs = model.generate(
    **batch_output,
    max_new_tokens=128,
    temperature=0.7
    )

RAG场景使用(待补充)

  • RAG 使用模板的示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    # 准备文档
    documents = [
    {
    "title": "2025年科技趋势报告",
    "text": "人工智能和机器学习技术将继续快速发展..."
    },
    {
    "title": "量子计算进展",
    "text": "量子计算机在特定问题上展现出巨大优势..."
    }
    ]

    # 用户问题
    conversation = [
    {"role": "user", "content": "2025年有哪些重要的科技趋势?"}
    ]

    # 使用RAG模板格式化
    formatted_input = tokenizer.apply_chat_template(
    conversation,
    documents=documents,
    add_generation_prompt=True,
    return_tensors="pt"
    )
  • 注:一般的模板不支持 documents,目前包括 Llama 系列,Qwen 系列等均不支持

  • 支持 documents 的模型示例:huggingface.co/CohereLabs/c4ai-command-r-v01/blob/main/tokenizer_config.json

  • chat_template 模版不支持 documents 时,方法包括:

    • 自己写 Jinja2 模板,把 documents 拼进 system 或第一条 user 消息,再在 vLLM 等框架启动时通过 --chat-template 指定,apply_chat_template 函数也支持该参数;
    • 直接在外部把检索结果拼接成普通字符串,再按常规 messages=[{"role":"user","content":"..."}] 传入即可
  • 最佳实践:先通过 Prompt Engineering 找到合适的模版,然后通过固定的模版文件将该形式固定下来,模型微调和线上 serving 均使用这个模版,这样可以避免因为模型微调和线上 serving 不一致带来的问题,也方便团队内外的合作


附录:最佳实践建议

  • add_generation_prompt 的使用: 在推理时,确保设置 add_generation_prompt=True 以获得正确的助手回复
  • 不同模型使用不同的 chat template,可使用 tokenizer.get_chat_template() 查看具体格式
  • 对于长对话,启用 truncation=True 并设置合适的 max_length
  • 批量处理时合理使用 padding 参数以提高效率,否则可能返回不同长度的编码结果
  • 添加适当的错误处理,特别是对于模板不支持的功能
  • 某些模型不支持 tools 参数,需要检查模型文档
  • 处理长序列时可能遇到内存问题,考虑减小 batch size 或 max_length
  • 确保 conversation 格式正确,每个消息都有 role 和 content 键

附录:关于 tools 类型

  • 在大模型工具调用场景中,code_interpreter(代码解释器)和 function(函数/工具)是两种不同类型的工具

function(函数/工具)

  • function(函数/工具)是预先定义的、具有特定功能的程序函数或API接口,用于让模型调用外部能力 ,模型通过生成符合格式的调用指令(如JSON),触发这些函数执行,并获取返回结果

    • 例如:天气查询接口、数据库查询函数、网页爬虫工具等,如模型调用get_weather(city="北京")函数获取实时天气
  • function 调用方式:模型需严格按照预设格式(如{"name": "函数名", "parameters": {"参数名": "值"}})生成调用指令,确保函数能被正确解析和执行,例如:

    1
    {"name": "translate", "parameters": {"text": "Hello", "target_lang": "zh"}}
  • function 灵活性低:功能固定,只能执行预定义的操作;但安全性高:严格限制在预设函数范围内,风险可控

  • function适用场景包括

    • 需要调用外部服务或系统(如查询实时数据、操作硬件设备)
    • 执行结构化任务(如数据库查询、API调用)
    • 功能固定、无需动态逻辑的操作(如格式转换、简单计算)

code_interpreter(代码解释器)

  • code_interpreter(代码解释器)是一个能够动态执行代码(通常是Python)的沙箱环境,允许模型直接生成并运行代码来解决问题,模型生成代码后,解释器会运行代码并返回输出结果(包括文本、图表等)

    • 例如:执行数学计算、数据可视化、处理 Excel 表格等,如 模型生成Python代码计算1+2+...+100的和,并通过代码解释器执行得到结果
  • code_interpreter 灵活性高:支持任意代码逻辑,可解决复杂、动态的问题;但安全性低:需运行用户/模型生成的代码,存在恶意代码风险(通常通过沙箱隔离缓解)

  • code_interpreter 中,模型直接生成代码片段(通常包裹在特定标记中,如

    ... ```),由解释器解析并运行,例如:
    1
    2
    3
    4
    ```python
    import numpy as np
    result = np.sum(range(1, 101))
    print(result)

  • code_interpreter 适用场景包括

    • 需要复杂逻辑计算(如统计分析、公式推导)
    • 数据处理与可视化(如绘制图表、处理CSV数据)
    • 临时编写简单脚本解决问题(如批量处理文本、解方程)
  • 使用 code_interpreter 时,只需要在 tools 里面加一项 { "type": "code_interpreter" },,这样 chat_template 会自动识别到该字段并输出一些使用信息,告诉模型如何给出代码,并告知模型这个代码可以被执行

    • 以 LongCat-Flash-Chat/blob/main/tokenizer_config.json 为例,其具体做法是先将 code_interpreter 包装成一个类似 function 的格式,再统一输出,最终效果就是让模型知道可以调用 code_interpreter 执行代码("code" 参数内容就是代码)

function 和 code_interpreter 整体对比

  • 注:在实际应用中,两者常结合使用:function处理外部交互,code_interpreter处理复杂计算,共同扩展大模型的能力边界
    维度 function(函数) code_interpreter(代码解释器)
    核心能力 调用预定义功能接口 动态执行代码逻辑
    适用场景 外部服务调用、结构化任务 复杂计算、数据处理、脚本生成
    调用格式 严格 JSON 格式指令 代码片段(如 Python)
    灵活性 低(固定功能) 高(支持任意逻辑)
    安全性 高 需沙箱隔离,风险较高

附录:chat-template 格式化

  • 大部分开源模型的 chat-template 都是压缩为一行的,可读性较差

  • 可以使用下面的代码重新存储 tokenizer 信息

    1
    tokenizer.save_pretrained(output_model_name)
  • 这样会同步生成得到的 chat-template.jinja 文件,整体格式是更可读的

  • 注:也可以使用大模型来帮忙格式化


附录:chat-template continue 语句的使用

  • 老版本的 transformers 中,调用 tokenizer.apply_chat_template 时 不支持 chat-template 中有 continue 语句
  • 若遇到类似下面的错误时,升级 transformers 版本后可以解决问题:
    1
    jinja2.exceptions.TemplateSyntaxError: Encountered unknown tag 'continue'. Jinja was looking for the following tags: 'elif' or 'else' or 'endif'. The innermost block that needs to be closed is 'if'.

附录:Qwen2-72B-Instruct chat-template 使用示例

  • Qwen2-72B-Instruct 的 chat-template 非常简单

  • 特别需要说明:当不增加 System Prompt 时, Qwen2-72B-Instruct 会默认将 "You are a helpful assistant." 作为 System Prompt

  • Qwen2-72B-Instruct/tokenizer_config.json 原始定义:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    {
    "add_prefix_space": false,
    "added_tokens_decoder": {
    "151643": {
    "content": "<|endoftext|>",
    "lstrip": false,
    "normalized": false,
    "rstrip": false,
    "single_word": false,
    "special": true
    },
    "151644": {
    "content": "<|im_start|>",
    "lstrip": false,
    "normalized": false,
    "rstrip": false,
    "single_word": false,
    "special": true
    },
    "151645": {
    "content": "<|im_end|>",
    "lstrip": false,
    "normalized": false,
    "rstrip": false,
    "single_word": false,
    "special": true
    }
    },
    "additional_special_tokens": ["<|im_start|>", "<|im_end|>"],
    "bos_token": null,
    "chat_template": "{% for message in messages %}{% if loop.first and messages[0]['role'] != 'system' %}{{ '<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n' }}{% endif %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}",
    "clean_up_tokenization_spaces": false,
    "eos_token": "<|im_end|>",
    "errors": "replace",
    "model_max_length": 131072,
    "pad_token": "<|endoftext|>",
    "split_special_tokens": false,
    "tokenizer_class": "Qwen2Tokenizer",
    "unk_token": null
    }
    • 注:Qwen2.5-72B-Instruct 的 chat-template 有调整,支持了 工具调用等,同时还修改了默认的 System Prompt 为 "You are Qwen, created by Alibaba Cloud. You are a helpful assistant."
      1
      2
      3
      4
      5
      {
      ...,
      "chat_template": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n {%- else %}\n {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}\n {%- endif %}\n {{- \"\\n\\n# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within <tools></tools> XML tags:\\n<tools>\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n</tools>\\n\\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\\n<tool_call>\\n{\\\"name\\\": <function-name>, \\\"arguments\\\": <args-json-object>}\\n</tool_call><|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0]['role'] == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0]['content'] + '<|im_end|>\\n' }}\n {%- else %}\n {{- '<|im_start|>system\\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) or (message.role == \"assistant\" and not message.tool_calls) %}\n {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {{- '<|im_start|>' + message.role }}\n {%- if message.content %}\n {{- '\\n' + message.content }}\n {%- endif %}\n {%- for tool_call in message.tool_calls %}\n {%- if tool_call.function is defined %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n<tool_call>\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {{- tool_call.arguments | tojson }}\n {{- '}\\n</tool_call>' }}\n {%- endfor %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n<tool_response>\\n' }}\n {{- message.content }}\n {{- '\\n</tool_response>' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}\n",
      ...,
      }
  • Qwen2-72B chat-template 使用示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    model_name = "/Users/xxx/llm/model/Qwen2-72B-Instruct"

    # load the tokenizer and the model
    tokenizer = AutoTokenizer.from_pretrained(model_name)

    messages = [
    {
    "role": "system",
    "content": "${system_prompt}"
    },
    {
    "role": "user",
    "content": "${user_round_0}"
    },
    {
    "role": "assistant",
    "content": "${assistant_round_0}"
    },
    {
    "role": "user",
    "content": "${user_round_1}"
    },
    {
    "role": "assistant",
    "content": "${assistant_round_1}"
    },
    {
    "role": "user",
    "content": "${assistant_round_2}"
    }
    ]
    output = tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
    print(output)
    # <|im_start|>system
    # ${system_prompt}<|im_end|>
    # <|im_start|>user
    # ${user_round_0}<|im_end|>
    # <|im_start|>assistant
    # ${assistant_round_0}<|im_end|>
    # <|im_start|>user
    # ${user_round_1}<|im_end|>
    # <|im_start|>assistant
    # ${assistant_round_1}<|im_end|>
    # <|im_start|>user
    # ${assistant_round_2}<|im_end|>
    # <|im_start|>assistant


    messages = [
    {
    "role": "system",
    "content": "${system_prompt}"
    },
    {
    "role": "user",
    "content": "${user_round_0}"
    },
    {
    "role": "assistant",
    "content": "${assistant_round_0}"
    },
    {
    "role": "user",
    "content": "${user_round_1}"
    },
    {
    "role": "assistant",
    "content": "${assistant_round_1}"
    }
    ]

    output = tokenizer.apply_chat_template(messages, add_generation_prompt=False, tokenize=False)
    print(output)
    # <|im_start|>system
    # ${system_prompt}<|im_end|>
    # <|im_start|>user
    # ${user_round_0}<|im_end|>
    # <|im_start|>assistant
    # ${assistant_round_0}<|im_end|>
    # <|im_start|>user
    # ${user_round_1}<|im_end|>
    # <|im_start|>assistant
    # ${assistant_round_1}<|im_end|>


    messages = [
    {
    "role": "user",
    "content": "${user_round_0}"
    },
    {
    "role": "assistant",
    "content": "${assistant_round_0}"
    },
    {
    "role": "user",
    "content": "${user_round_1}"
    },
    {
    "role": "assistant",
    "content": "${assistant_round_1}"
    }
    ]

    output = tokenizer.apply_chat_template(messages, add_generation_prompt=False, tokenize=False)
    print(output)
    # <|im_start|>system
    # You are a helpful assistant.<|im_end|>
    # <|im_start|>user
    # ${user_round_0}<|im_end|>
    # <|im_start|>assistant
    # ${assistant_round_0}<|im_end|>
    # <|im_start|>user
    # ${user_round_1}<|im_end|>
    # <|im_start|>assistant
    # ${assistant_round_1}<|im_end|>

补充:qwen_vl_utils.process_vision_info 函数的使用

  • 专为 Qwen-VL(视觉语言)系列模型(如 Qwen-VL、Qwen2.5-VL)设计

    • 主要作用是将多模态对话数据(包含图片或视频的文本结构)解析并读取为模型能够直接使用的 PIL Image 对象或 Torch Tensor(视频帧) ,同时整理视频处理的采样参数
  • 函数签名与参数详解

    1
    2
    3
    4
    5
    6
    def process_vision_info(
    conversations: Union[List[Dict[str, Any]], List[List[Dict[str, Any]]]],
    return_video_kwargs: bool = False,
    return_video_metadata: bool = False,
    image_patch_size: int = 14,
    ) -> Tuple[...]
    • 参数说明:
      参数 类型 说明
      conversations List[Dict] 或 List[List[Dict]] 必填,多模态对话内容,支持单轮对话(单层列表)或多轮对话(嵌套列表),
      每条字典中需包含 role(角色)和 content(内容),且 content 内部需含有 image、image_url 或 video 字段来指定视觉数据的路径或 URL
      return_video_kwargs bool 默认为 False,
      若为 True,返回值中会多一个字段(即完整的视频处理参数字典 video_kwargs)
      return_video_metadata bool 默认为 False,
      若为 True,返回值 video_inputs 中会包含 video_metadata 信息
      image_patch_size int 图像分块大小(Vision Transformer 的 Patch Size),通常与模型加载时的配置保持一致,影响图像缩放时的网格计算,
      默认为 14,是图片和视频共享参数
    • 返回值说明:函数返回一个 三元组 (image_inputs, video_inputs, video_kwargs) 或 二元组 (image_inputs, video_inputs):
      返回值 类型 说明
      image_inputs Optional[List[Image.Image]] 读取后的 PIL Image 对象列表(按输入顺序),
      如果输入中无图片,则为 None
      video_inputs Optional[List[Union[torch.Tensor, List[Image.Image]]]] 读取后的视频数据列表,
      具体是 torch.Tensor 还是 PIL Image 列表取决于 fetch_video 的底层实现,通常为按帧采样的张量,如果输入中无视频,则为 None
      video_kwargs Optional[Dict[str, Any]] 视频采样辅助参数,
      默认包含 {'do_sample_frames': False},若满足特定条件,还会包含视频的 fps(帧率)列表,用于后续模型处理时对齐时间维度,
      仅 return_video_kwargs=True 时返回

内部处理流程

  • 1)提取视觉信息 :调用 extract_vision_info(conversations) 递归解析对话结构,提取出所有包含 image、image_url 或 video 的字典片段
  • 2)遍历并读取数据 :
    • 若字段为 image 或 image_url,调用 fetch_image 读取图片(支持本地路径或 HTTP URL),返回 PIL Image 并加入 image_inputs
    • 若字段为 video,调用 fetch_video 读取视频(返回视频张量以及采样帧率 video_sample_fps),并加入 video_inputs
    • 若字段均不匹配,抛出 ValueError
  • 3)空值处理 :如果最终图片列表或视频列表为空,将其设为 None
  • 4)构造视频参数字典 :
    • 基础字典为 {'do_sample_frames': False}
    • 关键兼容逻辑 :若 return_video_metadata 为 False(兼容 Qwen2.5-VL 旧版本),则向字典注入 {'fps': video_sample_fps_list}
  • 5)返回结果 :根据 return_video_kwargs 决定是否返回 video_kwargs(若为 False,该值仍会返回,但通常不会被使用)

使用示例

  • 场景 1:仅处理单张图片(不用获取视频参数)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    from qwen_vl_utils import process_vision_info

    # 构造单轮对话,包含一张本地图片
    conversations = [
    {
    "role": "user",
    "content": [
    {"type": "image", "image": "/path/to/your/photo.jpg"},
    {"type": "text", "text": "描述这张图片"}
    ]
    }
    ]

    # 不用获取视频参数
    images, videos, video_kwargs = process_vision_info(conversations)

    # images 此时为 [<PIL.Image>],videos 为 None
    print(f"读取到 {len(images) if images else 0} 张图片")
  • 场景 2:同时处理图片和视频,并获取视频参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    conversations = [
    {
    "role": "user",
    "content": [
    {"type": "image", "image_url": "https://example.com/pic.png"}, # 支持URL
    {"type": "video", "video": "/path/to/video.mp4"},
    {"type": "text", "text": "对比视频和图片"}
    ]
    }
    ]

    # 开启返回视频参数
    images, videos, video_kwargs = process_vision_info(
    conversations,
    return_video_kwargs=True,
    return_video_metadata=False # 兼容旧版Qwen-VL
    )

    # 读取结果
    if images:
    print(f"图片数量: {len(images)}")
    if videos:
    print(f"视频张量列表: {videos}") # 通常为 [torch.Tensor]
    print(f"视频采样参数: {video_kwargs}") # 包含 do_sample_frames 和 fps
  • 场景 3:多轮对话结构(自动展平提取所有视觉信息,无需手动处理嵌套)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    multi_turn_convs = [
    [ # 第一轮
    {"role": "user", "content": [{"image": "img1.jpg"}]},
    {"role": "assistant", "content": [{"text": "这是第一张"}]}
    ],
    [ # 第二轮
    {"role": "user", "content": [{"image": "img2.jpg"}, {"video": "video.mp4"}]}
    ]
    ]

    images, videos,_ = process_vision_info(multi_turn_convs)
    # 函数会自动展平提取所有视觉信息,无需手动处理嵌套

Kimi-K3 (Kimi K3) 的 chat-template 说明

  • Kimi K3 没有使用 jinja 模版,而是调用 Python 文件直接实现编码,包含了多个 Python 文件和一个 BPE 模型文件 tiktoken.model (大约 2.5 M)

  • 编码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    from transformers import AutoProcessor, AutoConfig
    model_path = "/Users/jiahong/llm/model/Kimi-K3"
    processor = AutoProcessor.from_pretrained(model_path, trust_remote_code=True)

    # 读取本地图片
    from PIL import Image
    import numpy as np
    image = Image.fromarray(np.random.randint(0, 255, (256, 256, 3), dtype=np.uint8))

    messages = [
    {
    "role": "system",
    "content": "You are a helpful assistant!",
    },
    {
    "role": "user",
    "content": [
    {"type": "text", "text": "We have two images"},
    {"type": "text", "text": "\n# image1:"},
    {"type": "image", "image": image},
    {"type": "text", "text": "\n# image2:"},
    {"type": "image", "image": image},
    {"type": "text", "text": "\nWhat animal is on the candy?"},
    ]
    },
    ]
    prompt = processor.apply_chat_template(
    messages,
    add_generation_prompt=True,
    tokenize=False,
    thinking=True,
    thinking_effort="high",
    )
    print(prompt)
  • 编码结果:

    1
    2
    3
    4
    5
    <|open|>message role="system" type="thinking-effort"<|sep|>`thinking_effort` guides on how much to think in your thinking channel (not including the response channel), supported values include `low`, `medium`, `high`, and `max`.
    Now the system is invoked with `thinking_effort=high`.<|close|>message<|sep|><|end_of_msg|><|open|>message role="system"<|sep|>You are a helpful assistant!<|close|>message<|sep|><|end_of_msg|><|open|>message role="user"<|sep|>We have two images
    # image1:<|kimi_image_placeholder|>
    # image2:<|kimi_image_placeholder|>
    What animal is on the candy?<|close|>message<|sep|><|end_of_msg|><|open|>message role="assistant"<|sep|><|open|>think<|sep|>

附录:模型下载和导入问题

  • 加载 tokenizer 时可能会报错:

    1
    ValueError: Error parsing line b'version https://git-lfs.github.com/spec/v1' in /Users/jiahong/llm/model/Kimi-K3/tiktoken.model
  • 根因:tiktoken.model 在仓库里本是 Git LFS 托管文件

    • 首次(LFS 未拉取时)被读取过一次,tiktoken 的 read_file_cached 把当时那个 LFS 指针文本按 sha1(文件绝对路径) 缓存到了系统临时目录,比如 MacOS 是下面的临时目录:

      1
      /var/folders/.../T/data-gym-cache/d2ec580956c6e1aac12c4f0a8f1c09478d6a9c4f
    • 即使后来 LFS 真实内容(2.7MB 的 BPE 词表,首行 IQ== 0)已下载到位,tiktoken 仍优先读这个过期的缓存指针 ,于是把 version https://git-lfs.github.com/spec/v1 当成 token rank 来解析而报错

  • 关键点:缓存键是文件路径的 sha1 ,不校验文件内容/哈希(load_tiktoken_bpe 未传 expected_hash),所以文件变了缓存不会自动失效

  • 解决方案

    • 方案 A(推荐,根治) :删除该条过期缓存:

      1
      2
      3
      4
      import hashlib, tempfile, os
      blobpath = "/Users/jiahong/llm/model/Kimi-K3/tiktoken.model"
      cache_dir = os.path.join(tempfile.gettempdir(), "data-gym-cache")
      os.remove(os.path.join(cache_dir, hashlib.sha1(blobpath.encode()).hexdigest()))
      • 或直接清空整个缓存目录:rm -rf /var/folders/*/T/data-gym-cache(macOS)/ rm -rf /tmp/data-gym-cache(Linux)
    • 方案 B(临时绕过) :在 import tiktoken 之前禁用缓存:

      1
      2
      import os
      os.environ["TIKTOKEN_CACHE_DIR"] = "" # 空字符串 → read_file_cached 直接读源文件
    • 亲测:使用方案 A,删掉过期缓存条目后没有出现问题

1…313233…352
San Ye

San Ye

Stay Hungry. Stay Foolish.

704 posts
53 tags
© 2026 San Ye
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4