-
Agent评估,你真的做对了吗 在过去,评估大模型相对简单:我们只需要衡量回答对不对。无论是文本生成、问答,还是代码补全,评估对象通常都是模型的一次输出。但 Agent 不一样。Agent 不只是生成答案,而是在完成一个任务:它需要理解目标、规划步骤、调用工具、观察反馈,并在多轮交互中不断调整执行路径。这也让评估从一件“简单判断对错”的事情,变成了一个真实而复杂的工程问题。在实际开发中,团队往往会遇到几个非常具体的困难:● 评估成本高、速度慢很多评测仍然依赖人工标注,不仅成本高,而且周期长,严重拖慢 Agent 的迭代节奏。● 结果不稳定,难以复现同样的输入,在不同时间可能得到不同输出,评估像“抽盲盒”,很难做回归测试,也难以定位问题。● 过程不可见,难以诊断Agent 是真的一步步执行正确,还是“最后一步碰巧答对”,往往无法判断,问题也难以被准确归因。这些问题,使得评估不再只是一个辅助工具,而逐渐成为 Agent 工程化落地过程中的关键瓶颈。很多团队最终只能依赖经验、日志排查,甚至是“感觉”,来判断一个 Agent 到底表现好不好。也正是为了解决这些问题,我们在 AgentArts 中构建了一套面向 Agent 的评估体系。它不只关注最终答案是否正确,也关注任务执行过程中每一步是否合理、可控、可诊断。我们希望通过这套体系,把评估从上线前的一次性检查,变成贯穿开发、调试、上线与运行全过程的基础能力。从这张图可以看到,Agent 评估的对象已经不再只是一次输出,而是从用户输入、模型理解、工具调用、多轮交互到最终结果的完整执行过程。 Agent, 我们到底在评什么 每一个 Agent 开发者在构建 Agent 的过程中,都会面对一个核心问题:如何衡量自己的 Agent 效果。比如,做问答助手时,开发者关心的是 Agent 有没有真正解决用户问题;做新闻撰稿 Agent 时,更关心检索来源是否真实、是否具备时效性;做企业流程 Agent 时,还要关注工具调用是否正确、执行路径是否可控。但问题在于,这些关注点往往是分散的。大多数开发者很难同时覆盖所有维度,也因此常常会怀疑:这套评估到底是否全面、是否准确。在 AgentArts 中,我们将 Agent 评估收敛为三个核心维度:结果层:输出是否正确、有用、合规、安全过程层:执行路径是否合理,包括工具选择、参数填充、轨迹质量交互层:多轮过程中是否稳定,包括上下文保持、目标一致性这三个层次,本质上对应了工程中最关键的三个问题:结果对不对 → 过程是否合理 → 多轮是否稳定围绕这三层结构,AgentArts 内置了 40+ 经过业务验证的评估器,覆盖输出质量、执行路径与多轮交互等关键环节,并统一组织为一套完整的评估体系。下图展示的就是 AgentArts 的评估器体系:它不是单一指标,而是围绕结果、过程、交互三个层次,提供多维度、可组合的评估能力。在实际使用中,用户可以基于数据集,或直接调用线上 Agent 发起评测任务,系统会自动完成执行与打分。下面是一个典型的评估任务界面。用户可以选择评估对象、配置评估数据和评估指标,将原本依赖人工判断的过程,转化为可复用的自动化评测任务。评测任务执行后,系统会输出任务成功率、指标得分、样本明细等信息,帮助开发者快速判断当前 Agent 版本的整体表现。在实际运行中,评估效率直接决定了它是否能够被持续使用。从上图可以看到,在一个包含 30 条数据的评测任务中,整个执行过程仅耗时 37 秒,并完成了 100% 的任务成功率统计与多维指标分析。相比传统依赖人工评估的方式,这种自动化评测大幅降低了时间成本,使评估可以真正融入日常开发与迭代流程,而不是变成上线前偶尔执行一次的“重流程”。基于这一点,AgentArts 在工程实现上重点优化了评估的规模化与执行效率,使评估可以支撑真实业务中的持续迭代。目前系统具备以下能力:覆盖 Agent 全链路评估千条级数据评测可在小时级完成支持超长链路执行轨迹评估(上下文长度最高可达 2M)支持多版本策略的统一评估、对比与回退多指标多维度同时评估支持大规模数据并行执行评测任务支持智能评测集生成(仅需少于3 条样本即可构建高质量、多样化评测数据)支持 Code-based 规则评估与 LLM Judge,半数以上评估器准确率超过 90%,稳定对齐人类偏好 评估,如何真正发挥作用 如果说前面的能力解决的是“如何评估”,那么接下来的问题是:评估结果,如何真正参与系统优化?在 AgentArts 中,评估是沿着 Agent 的真实运行路径发生的。下图展示了 AgentArts 评估服务的整体链路:从用户输入开始,Agent 完成任务理解、工具调用和多轮交互;执行过程中产生的数据会通过 SDK 被采集,并在平台中结构化存储为 Trace;随后,这些 Trace 会进入评估引擎,按照结果层、过程层和交互层进行分析,最终输出评估结果和报告。如果只停留在这里,评估依然只是对“已经发生的执行”的一次分析。更重要的是:这些评估结果,能够反过来影响系统本身。因此,AgentArts 不是只做一次性打分,而是把评估接入到数据构建、在线运行、人工修正和版本迭代的完整流程中。① 从“没有数据”开始在实际开发中,第一个问题往往不是模型,而是数据。很多团队在构建 Agent 时,很难找到覆盖真实场景的评测集:要么数据过少,要么表达单一,更缺乏边界条件和长尾情况。在 AgentArts 中,用户可以直接通过自然语言描述任务场景,系统自动生成评测数据。下图展示的是智能评测集生成能力:开发者只需要提供少量样例或任务描述,系统就可以围绕场景语义进行扩展,生成更丰富的评测数据。系统会基于语义扩展生成多样化数据,包括:不同表达方式与输入风格边界条件与长尾场景正负样例与对抗数据从少量描述出发,即可构建一批接近真实分布的评测集。评估不再受限于“有没有数据”,而是可以主动生成覆盖范围。② 从“离线验证”走向“在线评估”有了评测集之后,下一步是评估执行。但在真实系统中,仅靠离线评测是不够的。很多问题只会在真实用户交互中暴露,而这些问题如果不能被及时捕捉,就很难定位。在 AgentArts 中,评估能力可以直接接入运行链路,实现在线评估。下图展示的是在线评估能力:系统可以基于真实用户请求,对 Agent 的输出结果、执行轨迹和多轮表现进行自动分析。系统可以自动完成:检测 AI 味、幻觉或不自然表达分析执行轨迹与工具调用质量在无参考答案的情况下完成质量判断对多轮对话中的目标一致性进行评估评估在这里不再是离线任务,而是持续运行的系统能力。③ 从“评估结果”走向“数据资产”当问题被检测出来之后,真正重要的是:这些问题如何被利用。在很多系统中,评估结果只停留在一次性的分析,无法进入下一轮优化。而在 AgentArts 中,所有评估结果都会被结构化沉淀,并支持人工参与。下图展示的是评估结果沉淀与人工修正流程:开发者可以查看具体样本、修正评估结果、标注高质量或问题样本,并将这些数据继续回流到评测集或训练数据中。 对评估结果进行人工修正标注高质量或问题样本将关键数据回流至新的评测集将沉淀数据用于后续训练或偏好对齐这些数据会成为后续版本的验证基准,甚至可以进一步用于模型训练,如强化学习或偏好对齐。 评估, Agent自优化驱动引擎 如果把前面的能力串联起来,就会形成一个完整闭环。下图展示的是 AgentArts 中的评估闭环:评估结果被转化为一组 可度量、可比较、可回归的反馈信号,作为 Agent 系统中的“决策依据”,直接参与关键决策:哪些改动是有效的,需要保留哪些策略出现退化,需要回退哪些问题持续出现,需要重点优化Harness Engineering 的视角来看,这些评估结果会持续驱动 Agent 不断优化 (人工或者自我演进),具体体现在:Prompt 如何重写记忆如何组织工具如何选择与调用工作流如何调整基座大模型是否需要强化对齐每一次迭代,都会进入同一套评估标准进行回归验证,再进入下一轮循环;当这个闭环稳定运行多轮之后,最终会沉淀出一个更强、更高效、更稳定的 Agent。这也是 AgentArts 评估服务想解决的核心问题:让评估从“判断工具”,变成“驱动系统进化的基础设施”。AgentArts 评估服务已在华为云平台上线,面向开发者与企业用户开放。体验地址: cid:link_0 关注魔方公众号,获取更多前沿资讯添加社区小助手k8s2222,进入技术交流群
-
一、背景介绍在大模型推理部署中,Prefill-Decode(P/D)分离是一种被广泛采用的性能优化架构。随着大语言模型参数量不断增长,推理过程中的计算资源消耗和延迟问题日益突出。传统的一体化推理架构难以同时优化首token延迟(TTFT)和整体吞吐率(TPOT),而P/D分离通过将推理过程拆分为两个独立阶段,让每个阶段使用最适合其计算特性的并行策略,实现了显著的性能提升。本文将详细介绍如何通过Kthena控制器,在昇腾NPU上部署DeepSeek-V4-Flash模型,完成P/D分离的实践。我们会深入解析P/D分离的技术原理、Kthena的编排能力,以及ModelRoute如何实现P/D实例的自动发现与KV传输协作。相关部署模板参考Kthena项目地址:cid:link_0 二、P/D分离技术原理2.1 大模型推理的两阶段解析大语言模型的推理过程是一个自回归生成过程,本质上分为两个截然不同的阶段:Prefill阶段(首token生成)Prefill阶段负责处理用户输入的prompt,将完整的输入序列通过模型前向传播,生成第一个输出token。这个阶段具有以下特征:计算密集型:需要处理整个输入序列,每个token都需要与所有输入token进行注意力计算并行度高:输入序列的所有位置可以并行计算,适合较大的张量并行(TP)规模内存访问模式:KV Cache首次生成,需要写入到高速缓存中延迟敏感:用户等待首token的时间直接影响体验在我们的配置中,Prefill阶段采用DP(数据并行)=2、TP(张量并行)=8的配置,利用更大的张量并行度来加速矩阵运算:--data-parallel-size 2 \--tensor-parallel-size 8 \--max-num-batched-tokens 8192 \--max-num-seqs 4 \Decode阶段(增量生成)Decode阶段是自回归生成过程,每次只处理一个新生成的token,并将其加入序列进行下一轮推理。这个阶段的特点与Prefill截然不同:Memory-Bound型:每次只计算一个token,但需要读取完整的KV Cache并行度需求低:单个token的计算量有限,过大的张量并行反而增加通信开销吞吐率敏感:需要最大化单位时间内生成的token数量KV传输频繁:每个Decode实例都需要访问所有Prefill实例生成的KV CacheDecode阶段采用DP(数据并行)=8、TP(张量并行)=2的配置,通过更大的数据并行来提升整体吞吐:--data-parallel-size 8 \--tensor-parallel-size 2 \--max-num-batched-tokens 144 \--max-num-seqs 48 \2.2 为什么P/D分离能提升性能传统的单体架构面临"一刀切"的困境:为了同时满足Prefill和Decode的需求,必须在并行策略上做出妥协,导致两个阶段都无法达到最优。而P/D分离的核心价值在于解耦:维度单体架构P/D分离张量并行折中值(如TP=4)Prefill用TP=8,Decode用TP=2数据并行固定值各角色独立扩展资源配置统一规格按需分配扩缩容整体调整独立扩缩容以DeepSeek V4为例:Prefill优化:更大的TP size(8)加速注意力计算,更大的批处理量(8192 tokens)提升吞吐Decode优化:更大的DP size(8)支持更多并发序列,更小的TP size(2)减少通信开销2.3 KV Cache传输机制P/D分离后,Prefill和Decode之间需要高效传输KV Cache,这是P/D分离架构中最关键的技术挑战之一。在我们的部署中,使用Mooncake Connector V1实现KV传输:--kv-transfer-config\'{"kv_connector": "MooncakeConnectorV1", "kv_role": "kv_producer", # Prefill为producer "kv_port": "9000", "engine_id": "$MOONCAKE_ENGINE_ID", ...}'Mooncake KV传输原理:Producer端(Prefill):生成的KV Cache通过Mooncake引擎传输到Decode节点Consumer端(Decode):从Mooncake引擎拉取KV Cache用于注意力计算Engine ID:每个P/D实例组有唯一的engine ID(${GROUP_NAME}_${ROLE_ID}),确保数据传输到正确的目标# Prefill配置"kv_role":"kv_producer"# Decode配置"kv_role":"kv_consumer"Mooncake具备昇腾NPU优化的高性能通信库,能够实现节点间KV Cache的低延迟传输,是P/D分离能够实际落地的关键技术。三、为什么P/D分离需要适配的Router3.1 路由在P/D分离架构中的核心作用在P/D分离架构中,请求的路由不再是简单地分发到某个后端实例,而是需要智能地协调Prefill和Decode两个阶段,这与传统微服务路由有本质区别。传统微服务路由:Client Request → Router → Backend Instance (处理完整请求)P/D分离路由:Client Request → Router → Prefill Instance (生成首token) ↕ (KV传输) Decode Instance (完成剩余生成)3.2 路由需要解决的挑战挑战一:请求生命周期管理一个完整的请求在P/D架构中需要经过多个阶段:请求首先路由到Prefill处理Prefill生成首token后,需要将请求转交给DecodeDecode可能需要多轮生成,每轮都涉及KV传输最终结果需要汇总返回给用户这要求Router必须理解P/D的请求流程,而不仅仅是做简单的负载均衡。挑战二:P/D实例发现与匹配Prefill和Decode实例数量可能不同(1P2D、2P1D等)请求需要在正确的Prefill和Decode实例之间传递需要跟踪每个请求当前由哪个实例处理挑战三:KV传输的协调KV Cache的传输需要满足以下条件:Prefill生成的KV必须传输到处理该请求的Decode实例Mooncake的engine_id需要正确配置以匹配P/D实例传输超时和错误处理挑战四:流量分配策略不同场景下可能需要不同的P/D配比:计算密集型场景:增加Prefill副本IO密集型场景:增加Decode副本Router需要支持动态调整流量分配3.3 ModelRoute的适配设计Kthena的ModelRoute正是为解决上述挑战而设计的适配层。它不仅负责基本的请求路由,还需要理解P/D分离的语义,协调Prefill和Decode的协作。四、ModelRoute配置详解4.1 整体配置结构apiVersion:networking.serving.volcano.sh/v1alpha1kind:ModelRoutemetadata:name:deepseek-v4namespace:defaultspec:modelName:"deepseek_v4"rules:-name:"default" targetModels: -modelServerName:"deepseekv4-pd"---apiVersion:networking.serving.volcano.sh/v1alpha1kind:ModelServermetadata:name:deepseekv4-pdnamespace:defaultspec:inferenceEngine:vLLMmodel:"deepseek_v4"workloadPort: port:7100 protocol:httpworkloadSelector: # 工作负载选择器 matchLabels: modelserving.volcano.sh/name:deepseekv4-pd # 这是最基本的标签匹配,用于识别属于该服务的所有Pod。 pdGroup: # P/D分组配置 groupKey:"modelserving.volcano.sh/group-name"# 指定用于分组的标签key prefillLabels: # 标记哪些Pod是Prefill角色 modelserving.volcano.sh/role:prefill decodeLabels: # 标记哪些Pod是Decode角色 modelserving.volcano.sh/role:decodetrafficPolicy: timeout:"300s" retry: attempts:3 retryInterval:"150ms"kvConnector: type:mooncake其中P/D分组配置(pdGroup)是实现P/D识别的关键配置:groupKey:指定用于分组的标签key,具有相同groupKey值的Pod属于同一个P/D实例组。这解决了一个关键问题:当我们部署多个P/D实例组时(如2×(1P1D)),Router需要知道哪些Prefill和Decode属于同一个组。prefillLabels:标记哪些Pod是Prefill角色。decodeLabels:标记哪些Pod是Decode角色。4.2 实例发现的实现机制Kthena控制器通过以下步骤实现P/D实例的自动发现:步骤1:标签注入在deepseek-serv.yaml中定义的ModelServing资源会被Kthena控制器处理,自动为每个Pod注入标签:metadata: labels: modelserving.volcano.sh/name:deepseekv4-pd modelserving.volcano.sh/group-name:<group-id> # 自动生成 modelserving.volcano.sh/role:prefill/decode # 根据role名称 modelserving.volcano.sh/role-id:<role-id> # 自动生成步骤2:标签查询ModelRoute的pdGroup配置会被控制器用于查询匹配的Pod:1. 查询所有 labels[modelserving.volcano.sh/name] = deepseekv4-pd 的Pod2. 按 labels[modelserving.volcano.sh/group-name] 分组3. 在每个组内,识别prefillLabels和decodeLabels匹配的Pod步骤3:动态维护当发生扩缩容时:新Pod创建后自动获得标签Router实时感知Pod变化无需手动更新配置4.3 KV传输的协调ModelRoute通过以下配置协调KV传输:kvConnector: type:mooncakeMooncake Connector的工作流程:1️⃣ Prefill启动时:从环境变量获取GROUP_NAME和ROLE_ID构建engine_id = ${GROUP_NAME}_${ROLE_ID}启动Mooncake Server,监听KV请求2️⃣ Decode启动时:使用相同的GROUP_NAME但不同的ROLE_ID配置为kv_consumer角色通过engine_id连接到对应Prefill的Mooncake Server3️⃣ 请求处理时:Prefill处理用户输入,生成KV CacheKV Cache通过Mooncake传输到对应Decode实例Decode使用接收到的KV继续生成4.4 流量策略配置trafficPolicy: timeout:"300s" retry: attempts:3 retryInterval:"150ms"这些配置确保了请求在P/D之间的可靠传递:timeout:整个生成过程可能需要较长时间,设置5分钟超时retry:如果请求在Prefill或Decode环节失败,自动重试五、Kthena编排的核心优势5.1 声明式编排简化运维传统方式下,部署P/D分离架构需要:手动创建和管理多组Deployment/Service手动配置Service之间的发现机制手动管理标签选择器和Endpoints使用Kthena的ModelServing,仅需声明式配置:apiVersion:workload.serving.volcano.sh/v1alpha1kind:ModelServingmetadata:name:deepseekv4-pdspec:replicas:1template: roles: -name:prefill replicas:1 -name:decode replicas:1控制器会自动完成:创建和管理Prefill/Decode的Pod注入必要的标签(group-name、role、role-id)设置健康检查配置资源限制5.2 灵活的P/D比例调整单实例1P1D:roles:-name:prefill replicas:1-name:decode replicas:1调整为2P1D(提升Prefill吞吐):roles:-name:prefill replicas:2 # 从1改为2-name:decode replicas:1调整为1P2D(提升Decode吞吐):roles:-name:prefill replicas:1-name:decode replicas:2 # 从1改为2部署2组独立的1P1D实例(横向扩展):spec: replicas:2 # 整体副本数设为2,每组自动生成1P1D我们测试了以下几种灵活的P/D比例配置:部署模式Prefill replicasDecode replicas说明1P1D11基础配置2P1D21提升Prefill吞吐,适合输入长度较大的场景1P2D12提升Decode吞吐,适合输出长度较大的场景2×(1P1D)2组P/D实例2组P/D实例横向扩展,整体吞吐翻倍所有这些调整都只需修改yaml配置并执行:kubectl apply -f deepseek-serv.yamlKthena控制器会自动处理Pod的创建、销毁和负载均衡。5.3 自动服务发现在传统的P/D部署中,需要手动配置服务发现:Prefill服务需要知道所有Decode实例的地址Decode服务需要知道所有Prefill实例的地址扩缩容时需要手动更新配置Kthena通过pdGroup和标签机制实现了自动服务发现:新增的Prefill或Decode实例自动被Router识别使用相同的group-key的实例自动组成P/D组无需手动配置实例地址六、部署实践6.1 模型准备为了加速部署和启动过程,我们将DeepSeek-V4-Flash模型权重预先下载到所有计算节点的 /models/DeepSeek-V4-Flash-w8a8-mtp 目录下。该目录应包含完整的模型权重文件、配置文件以及chat_template.jinja模板文件。从ModelScope下载模型权重:# 安装ModelScopepip install modelscope# 下载DeepSeek V4 Flash模型modelscope download --model Eco-Tech/DeepSeek-V4-Flash-w8a8-mtp --local_dir /models/DeepSeek-V4-Flash-w8a8-mtp如果使用git-lfs进行大文件管理,也可以:# 安装git-lfsgit lfs install# 克隆模型仓库GIT_LFS_SKIP_SMUDGE=1 git clone https://www.modelscope.cn/Eco-Tech/DeepSeek-V4-Flash-w8a8-mtp.git /models/DeepSeek-V4-Flash-w8a8-mtp# 进入目录并下载LFS大文件cd /models/DeepSeek-V4-Flash-w8a8-mtpgit lfs pull注意事项:确保所有计算节点的模型目录路径一致(/models/DeepSeek-V4-Flash-w8a8-mtp)可以在共享存储(如NFS)上预先下载模型,然后挂载到各个节点模型下载完成后,建议验证文件完整性:ls -la /models/DeepSeek-V4-Flash-w8a8-mtp/# 应包含 config.json, model.safetensors, chat_template.jinja 等文件6.2 完整部署流程Step 1: 创建ConfigMap(包含启动脚本)kubectl apply -f config.yamlconfig.yaml定义了Prefill和Decode的启动脚本,包含:环境变量配置vLLM启动参数Mooncake KV连接配置Step 2: 部署ModelServingkubectl apply -f deepseek-serv.yaml创建完整的P/D分离实例,包括:Prefill实例(1副本)Decode实例(1副本)自动注入的标签和配置Step 3: 配置路由kubectl apply -f modelRoute.yaml创建ModelRoute和ModelServer两个对象:ModelRoute:定义路由规则ModelServer:定义后端服务6.2 验证部署# 查看ModelServing状态kubectl get modelserving deepseekv4-pd# 查看所有相关Podkubectl get pods -l modelserving.volcano.sh/name=deepseekv4-pd# 查看Pod详情(确认标签)kubectl get pods -l modelserving.volcano.sh/name=deepseekv4-pd -o wide# 检查Pod日志kubectl logs -l modelserving.volcano.sh/role=prefillkubectl logs -l modelserving.volcano.sh/role=decode6.3 扩缩容操作扩缩容Prefill:# 编辑配置kubectl edit modelserving deepseekv4-pd# 将 prefill.replicas 从 1 改为 2# 或者使用patchkubectl patch modelserving deepseekv4-pd --type='json' \ -p='[{"op": "replace", "path": "/spec/template/roles/0/replicas", "value": 2}]'扩缩容Decode:kubectl patch modelserving deepseekv4-pd --type='json' \ -p='[{"op": "replace", "path": "/spec/template/roles/1/replicas", "value": 2}]'七、总结通过本次实践,我们验证了 Kthena 在昇腾 NPU 环境下部署 DeepSeek-V4-Flash 模型 P/D 分离(Prefill/Decode Disaggregation) 架构的完整可行性。主要成果:成功实现P/D分离部署,Prefill和Decode独立运行通过Mooncake KV传输实现P/D协作支持灵活的P/D比例调整(1P1D、2P1D、1P2D等)支持多实例横向扩展Kthena的核心价值:简易性:声明式配置替代复杂的手动编排,控制器自动处理Pod管理和标签注入灵活性:通过replicas字段即可独立调整P/D比例,无需修改其他配置自动发现:pdGroup机制实现P/D实例的自动识别和配对KV协调:与Mooncake深度集成,确保KV传输的正确路由综上所述,P/D 分离是提升大模型分布式推理效能的核心技术路径,而 Kthena 的适配 Router 设计与编排逻辑,为这一复杂架构在生产环境中的标准化部署提供了确定性的方案,确保了大模型服务在昇腾算力底座上的高效稳定运行。 相关部署模板参考Kthena项目地址:cid:link_0Kthena GitHub地址:cid:link_1 更多信息,欢迎访问 Kthena 官网: https://kthena.volcano.sh/欢迎Star★,Fork,来 Kthena 社区一起玩转LLM推理! 扫码回复“Kthena” 进入技术群
-
Kthena 是一个专为 Kubernetes 设计的云原生、高性能 LLM 推理路由和编排、调度系统。它旨在解决在生产环境中大规模编排、部署和服务 LLM 所面临的核心挑战,通过其独特的超节点拓扑感知的亲和性调度,KV Cache 感知的流量调度、Prefill/Decode 分离路由等高级功能,显著提升 GPU/NPU 资源利用率和吞吐,降低推理延迟,赋予企业前所未有的灵活性和控制力。作为 Volcano[1] 的子项目,Kthena [2] 致力于扩展 AI 训练之外的边界,打造训推一体的完整解决方案。Kthena 最新发布的 Kthena v0.4.0[3] 版本进一步简化了大语言模型(LLM)工作负载的管理,为 AI 基础设施赋能。该版本源于社区贡献者在过去两个月中的倾力付出与通力合作,运行更加稳定,功能更加丰富。 Kthena v0.4.0 关键特性 ▍更快、更智能的路由器 (Router)确定且高效的模型选择此前,由于 Kubernetes 内置的 CRD 校验无法强制实现跨对象的全局唯一性,将多个 ModelRoute 资源映射到同一个模型时可能会引发路由冲突,导致规则匹配出现歧义和目标选择的不一致。在Kthena v0.4.0,我们优雅地解决了这一问题。Kthena v0.4.0 引入了可靠的冲突解决机制[4]。当存在重复的 ModelRoute 时,路由器会确定性地优先选择最早创建(通常是预建)的路由,并将较新的重复项视为较低优先级。这确保了每次路由请求都能获得可预测且稳定的结果。可配置的Prefix-Cache统一的标准往往无法满足所有场景的需求。因此,Kthena 将硬编码的Prefix-Cache参数替换成了完全可配置的Prefix-Cache系统[5]。您现在可以通过以下参数对Prefix-Cache的行为进行细粒度的控制:Block Size(哈希处理块大小): 控制前缀匹配的粒度。较小的块能够提供更精确的匹配,但会增加 CPU 开销;而较大的块处理速度更快,但精准度会下降。Max Block Limits(最大块限制): 设定了对给定提示词(prompt)进行哈希处理的上限。避免路由器在处理过长的传入提示词时,出现延迟激增。Cache Capacity(缓存容量): 定义了路由器可以记住的前缀条目数量。增加此值可以提高多样化工作负载的缓存命中率,代价是略微增加内存占用。Top-K Results(Top-K 结果数): 决定在匹配成功时考虑多少个候选实例。调整此参数有助于实现更好的负载均衡,确保流量平稳地分布在多个节点上,而不是让单个活跃实例过载。通过微调这些设置,您可以定制 Kthena Router的Prefix-Cache,以更好地适配多样的模型和业务 LLM 工作负载。▍细粒度、资源高效的滚动更新过去,Kthena 能在整个 ServingGroup 级别执行滚动更新。但对于大规模的大型语言模型应用而言,完全重建一个 ServingGroup 往往是一个极其消耗资源且耗时的过程。为了解决这个问题,我们引入了基于 Role 的滚动更新机制[6]。当只有特定的 Role 需要变更时,您无需再更新整个 ServingGroup(这也是我们在恢复策略中引入 RoleRecreate 的原因)。从 v0.4.0 开始,可以动态调整 rolloutStrategy——这将大幅降低升级时的资源消耗,并显著缩短升级时ServingGroup的不可用时间。▍支持SGLang和vLLM的PD分离部署PD 分离部署架构已经成为了大规模 LLM 服务的标准架构。在 Kthena v0.4.0 中,Kthena 的modelServing和Router现已通过全面验证,能够良好地支持 vLLM 和 SGLang 的PD分离部署。用户可以根据需求通过 ModelServing 配置 Prefill 和 Decode。并结合 ModelServer 中的 pdGroup 配置实现 PD 感知的智能路由,从而轻松构建高效的 PD 分离推理服务。▍提升可观测性Role 状态可见性为了最大限度地降低 kube-apiserver 的负载,Kthena 的 ModelServing 使用了本地存储缓存 ServingGroup 和 Role 的状态。虽然这种方式效率很高,但我们也意识到,这限制了 Kthena 的可观测性,使整个 reconcile 过程中ServingGroup和Role的状态变化变成了一个黑盒,无法观测。在 v0.4.0 中,我们打破了这种“黑盒”状态。现在,我们能够直接通过 Kubernetes Events 暴露 Role 的状态[7],从而显著提升了 ModelServing 的可观测性。在未来的规划中,我们还计划根据用户需求将关键的 Role 信息直接嵌入到 ModelServing 的 Status 中,为您提供全面且透明的部署状态掌控能力。为开发者提供debug-port,可以直接拉取本地缓存中的ServingGroup和Role的状态。全面的访问日志现在,Router 会生成更详细的访问日志,为每一个请求捕获更丰富多样的路由元数据 (routing metadata)[8]。以下是 Kthena v0.4.0 Router 日志的一个示例:[2026-04-16T07:33:08.435627146Z] "POST /v1/chat/completions HTTP/1.1" 200 model_name=deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B model_router=deepseek-r1-1-1.5b model_server=deepseek-r1 selected_pod=deepseek-r1-1-1.5b-6989c66877-p6jvv request_id=ad683d1b-6011-4b0f-b9b5-cbb18d43c57b gateway=dev/default http_route=kthena-e2e-gie-8eoas/llm-route inference_pool=kthena-e2e-gie-8eoas/deepseek-r1-1-1.5b tokens=10/38 timings=3ms(0+2+0)与之前的版本相比,我们新增了 gateway、http_route 和 inference_pool 字段,以便对 Gateway 及 Gateway Inference Extension 的流量提供丰富的信息。▍开放的生态系统Kthena 致力于与广大的开源社区一道,将社区建设成为一个开放、包容且繁荣的项目。支持 ModelScope (魔搭社区)在 v0.4.0 中,Kthena 的模型下载器中扩展了对 ModelScope 协议的支持[9]。这使得用户和运维管理人员可以更加灵活地选择符合其项目需求的模型仓库。Kthena 拒绝绑定单一技术栈,而是坚持与多样化的 AI 技术无缝集成,从而不断深耕于充满活力的云原生 AI 生态之中。社区热忱地邀请全球开发者加入,共同构建包容且充满机遇的未来数字基础设施。 Kthena v0.4.0 贡献者 @YaoZengzeng@VanderChen@hzxuzhonghu@Tweakzx@pierluigilenoci@lihua871205@nalan2012@Murphylu1993@xrwang8@thisisharsh7@vyagh@FAUST-BENCHOU@LiZhenCheng9527@aabhinavvvvvvv@anirudh240@blenbot@sicaario@katara-Jayprakash@WHOIM1205@agrawalcodes 相关链接[1] Volcano: https://volcano.sh/[2] Kthena : https://kthena.volcano.sh/[3] Kthena v0.4.0: cid:link_0[4] 冲突解决机制: cid:link_1[5] 可配置的Prefix-Cache系统: cid:link_2[6] 基于 Role 的滚动更新机制: cid:link_3[7] 直接通过 Kubernetes Events 暴露 Role 的状态: cid:link_4[8] 路由元数据 (routing metadata): cid:link_5[9] ModelScope 协议的支持: cid:link_6 扫码回复“Kthena” 进入技术群
-
欢迎阅读「从零开始理解大模型」系列 —— 十篇文章,从"下一个词预测"到完整的大模型心智模型。每篇配可运行代码。第一篇:一切从"猜下一个词"开始第二篇:Token——大模型眼中的"字"长什么样第三篇:向量与 Embedding——把文字变成数学第四篇:Attention——大模型的"阅读理解"机制第五篇:Transformer 全景——积木怎么搭成大厦第六篇:训练——70 亿个参数是怎么"学"出来的第七篇:推理——你按下回车后的这一秒发生了什么第八篇:上下文窗口——大模型的"工作记忆"第九篇:Scaling Law——为什么"大力出奇迹"有效第十篇:从大模型到 Agent——下一个词预测如何长出手脚作者:十一前四篇,我们一块一块地认识了积木:Token(第二篇)—— 把文字切成模型认识的碎片Embedding(第三篇)—— 把碎片变成带语义的向量Attention(第四篇)—— 让每个词"看见"上下文但光有积木还不够。Attention 只是 Transformer 一层的一半,另一半是什么?GPT-2 有 12 层,LLaMA 有 32 层,为什么要叠这么多?这篇把积木搭成完整的大厦。读完之后,大模型从输入到输出的每一步,你都明白了。 ▍一、先说结论一句话版本:Transformer = (Attention + FFN) × N 层,再加点"胶水"。就这么简单的结构,重复 N 次,就是所有大模型的骨架。 ▍二、先打个比方,建立全局感觉想象一家公司,一份报告要经过 12 个部门审阅。每个部门固定做两件事:开会(Attention)—— 大家坐在一起,互相交流信息。每个人听完别人的发言,更新自己的理解。"你们都说说,我综合一下。"回工位写总结(FFN)—— 开完会,每个人回去独自思考,把讨论内容消化成自己的判断。"我自己想想,这事到底意味着什么。"另外有两个保障机制:保留原件(残差连接)—— 每次修改,都在原件上叠加,不会覆盖原件。"改归改,原来的东西不能丢。"统一格式(LayerNorm)—— 每次流转前,把报告里的数字拉到统一范围。"大家别一个写米、一个写公里,先统一度量衡。"12 个部门审完,报告里既有所有部门的意见,又没丢失原始信息。这就是 Transformer 干的全部事情。 下面一个一个展开。 ▍三、一层 Transformer 长什么样画成流程图:输入向量 │ ▼┌─────────────────────────────────────┐│ ① LayerNorm(统一格式) ││ ② Multi-Head Attention(开会) ││ ③ 残差连接:输出 = 输入 + 开会结果 │├─────────────────────────────────────┤│ ④ LayerNorm(统一格式) ││ ⑤ FFN(独立思考) ││ ⑥ 残差连接:输出 = 输入 + 思考结果 │└─────────────────────────────────────┘ │ ▼输出向量(送去下一层继续处理)上半部分是 Attention——让词和词之间互相交流。下半部分是 FFN——让每个词自己消化信息。中间的 LayerNorm 和残差连接做稳定保障。写成代码,核心就四行:residual = hiddenhidden = residual + attention(layernorm_1(hidden)) # 开会 + 保留原件residual = hiddenhidden = residual + ffn(layernorm_2(hidden)) # 独立思考 + 保留原件看着是不是特别朴素?别小看它——GPT-4、Claude、LLaMA、DeepSeek,内部全是这四行在不断重复。下面逐个讲解三个你还没见过的新组件。 ▍四、FFN——"开完会了,自己坐下来想一想"4.1 光开会还不够第四篇讲的 Attention,让每个词都"听到了"其他词的信息。处理 "Thank you very" 的时候,"very" 知道了前面有 "Thank" 和 "you"。但"听到"不等于"想明白"。就像你参加了一场会,听了一大堆发言,脑子里全是信息碎片。你还得回到工位上坐下来,自己理一理:**"这些信息综合起来,到底说明什么?"**这个"坐下来理一理"的步骤,就是 FFN(Feed-Forward Network,前馈网络)。为什么叫"前馈"?因为数据只往前走,不回头——当前 token 的信息处理完后,直接"喂"给下一层,不会把自己的输出再喂回自己。没有循环、没有反复,一路向前。4.2 它到底怎么做的公式长这样:FFN(x) = W₂ · GELU(W₁ · x + b₁) + b₂这里的 W₁、W₂ 是训练时学出来的权重矩阵,训练完就固定不变了。同一层里所有 token 共用同一组 W₁、W₂——不是每个 token 各有一份。别被公式吓着。拆开来看就是三步:输入(768 个数字) │ ▼ 第一步:"展开"—— 乘一个大矩阵,768 维变成 3072 维 │ 就好比把一页笔记铺开到四页纸上,有更多空间做标注 │ ▼ 第二步:"划重点"—— 用激活函数(GELU)过一遍 │ 就好比拿一支半透明荧光笔划重点: │ - 特别重要的内容(正数)会被高亮保留 │ - 不太重要的内容(负数)也不会被完全涂黑,而是被轻轻压低、保留一点点痕迹 │ ▼ 第三步:"压缩"—— 再乘一个大矩阵,3072 维压回 768 维 │ 就好比把四页标注总结回一页精华 │ ▼ 输出(还是 768 个数字)有一个特别重要的特点:FFN 只看自己,不看别人。 "very" 做 FFN 的时候,完全不管 "Thank" 和 "you" 在干嘛。这和 Attention 正好反过来——Attention 是"大家一起交流",FFN 是"每个人独自思考"。4.3 为什么说 FFN 是"知识库"有一个有趣的发现:大模型记住的事实(比如"法国首都是巴黎")主要存在 FFN 的权重里。打个比方:Attention 是搜索引擎——在上下文里搜到了 "Thank you" 这个线索FFN 是大脑里的记忆——看到这个线索后,从记忆中调出 "much" 这个最强搭配一个负责找线索,一个负责给答案。两者配合,才能又找对又答对。 ▍五、残差连接——"改归改,原件必须留着"5.1 世界上最简单的操作输出 = 输入 + f(输入) // f 是 Attention 或 FFN就是把输入原封不动加到输出上。对,就这么简单。但为什么叫"残差"? 移项一下就明白了:f(输入) = 输出 - 输入 = 残差Attention 和 FFN 学到的东西,不是"完整答案该长啥样",而是"跟输入差多少"。这个差值就叫残差。学一个小的修正量,比从零学一个完整答案简单多了。这就是残差学习的核心思想,名字也从这来。5.2 没有它,12 层就废了想想"传话游戏":你悄悄告诉第一个人一句话,一个传一个,传 12 轮——最后一个人说出来的,跟原话八竿子打不着。没有残差连接的 Transformer 就是这样。12 层一叠,原始信息早就面目全非了。而且训练的时候,调参数的信号(叫"梯度")要从第 12 层一路传回第 1 层,每过一层就弱一点。传 12 层下来,信号几乎没了——前面的层调不动参数,就等于白训练。这个问题叫梯度消失,是深层网络最经典的老大难。5.3 加上它之后,画风完全不同残差连接等于给每一层装了一条直通管道:输入 ──────┬──────────────────────────────────┐ │ │ ▼ │ 直通管道 Attention/FFN 处理 │(原始信息直接穿过去) │ │ ▼ │ 修正量(Δ)───────→ (+) ◄────────────────┘ │ ▼ 输出 = 输入 + Δ不管 Attention/FFN 处理得多好或多烂,原始输入都通过直通管道完整到达了终点。所以每一层不需要"从零开始写一份新报告",只需要在原报告上批注几句修改意见:Layer 1 输出 = Embedding + Δ₁ ← 第一个部门批了几句Layer 2 输出 = Embedding + Δ₁ + Δ₂ ← 第二个部门又批了几句...Layer 12 输出 = Embedding + Δ₁ + ... + Δ₁₂ ← 12 个部门的修改意见叠在一起就像改文章——不是每一稿从头写,而是在上一稿基础上改红字。改 12 轮,肯定比从零写 12 遍容易得多。用 transformer_anatomy.py[1] 可以验证:即使经过 12 层变换,输出和原始 Embedding 的余弦相似度还是很高。原始信息确实通过直通管道保到了最后。 ▍六、LayerNorm——"每次传递前,先统一度量衡"6.1 为什么需要你去菜市场买菜,一个摊位标价"3",另一个标价"3000"。哪个贵?不知道——因为一个单位是"元/斤",另一个是"元/吨"。数字大小没意义,除非单位统一。Transformer 里也是这样。向量经过矩阵乘法和加法之后,有些维度的数字飙到 100,有些缩到 0.001。差了十万倍。后续的 Softmax 看到这种极端分布就懵了:它会把几乎全部注意力给最大的那个值,其他值全变成零。数字大小太离谱,后面的计算全带跑偏了。6.2 怎么解决LayerNorm 干的事特别朴素:把向量里所有数字"拉"回差不多大的范围。LayerNorm(x) = γ · (x - μ) / σ + β拆开来看:先算平均值(μ),把所有数字都减去平均值——让中心归零再算散开程度(σ),除以它——让大小统一最后乘以 γ 加上 β——留一点微调余地(γ 和 β 是训练时学出来的)看个具体例子:输入: [100.0, 0.001, -50.0, 25.0] ← 差距十万倍,后续计算会炸 ↓ 减均值 18.75,除标准差 55.9输出: [1.45, -0.34, -1.23, 0.11] ← 都在 -2 到 +2 之间了,好处理多了就像考试成绩——原始分可能是 150 分满分、也可能是 10 分满分,没法比。但转成"标准分"之后,所有人的成绩都在同一个尺度上了。LayerNorm 的参数很少——GPT-2 每层只有 3072 个——但缺了它模型就训不出来。真正的四两拨千斤。 ▍七、拆开 GPT-2,看参数都在哪四个组件都认识了。现在打开 GPT-2 的"引擎盖",看看 1.24 亿个参数到底怎么分配的。先算一下每个组件有多少参数。GPT-2 的关键数字:向量维度 d=768,注意力头数 12,FFN 扩展倍数 4,词表 50257,最大位置 1024。Embedding:Token Embedding:每个 token 一个 768 维向量 → 50257 × 768 = 38,597,376Position Embedding:每个位置一个 768 维向量 → 1024 × 768 = 786,432Attention(每层):Q、K、V 三个权重矩阵,每个 768×768 → 3 × 589,824 = 1,769,472输出投影矩阵 O:768×768 = 589,824四个偏置项:768 × 4 = 3,072每层合计:约 236 万FFN(每层):W₁ 扩展矩阵:768 × 3072 = 2,359,296W₂ 压缩矩阵:3072 × 768 = 2,359,296两个偏置项:3072 + 768 = 3,840每层合计:约 472 万(是 Attention 的两倍,因为有 4 倍扩展)LayerNorm(每层):每层两组(Attention 前 + FFN 前),每组有 γ 和 β 各 768 个 → 768 × 2 × 2 = 3,072现在看运行 transformer_anatomy.py[1] 得到的实际分布:组件 参数量 占比──────────────────────────────────────────────────Token Embedding 38,597,376 31.0% ███████████████Position Embedding 786,432 0.6%Attention(12层合计) 28,348,416 22.8% ███████████FFN(12层合计) 56,669,184 45.5% ██████████████████████LayerNorm + 其他 38,400 0.0%三个有意思的发现:FFN 是最大头。 占 45.5%,几乎是 Attention 的两倍。为啥?因为 FFN 有 4 倍扩展(768→3072→768),两个大矩阵。FFN 是"知识库"嘛,存知识当然需要空间。Embedding 在小模型里占比惊人。 GPT-2 的 Embedding 占了 31%。但在更大的模型(比如 LLaMA 7B)里,Embedding 只占 2% 左右——因为模型变大的时候,Transformer 层的参数增长速度远超 Embedding。LayerNorm 小到可以忽略。 只有 38,400 个参数。但没它整个模型就训练不了。最不起眼的组件,反而最不能少。完整拆解和数据流追踪代码见附件 transformer_anatomy.py[1]。 ▍八、为什么要叠这么多层GPT-2 叠了 12 层,LLaMA 7B 叠了 32 层。能不能把一层做得特别大,只用一层搞定?不行。原因有两个。8.1 不同层看不同层次的东西就像人理解一句话,有从浅到深的过程:第三篇的实验展示过:同一个词 "bank",在"银行"和"河岸"两个语境下,向量差异是一层一层加大的:Layer 0: 相似度 1.000 ← 完全一样("这个 bank 啥意思我还不知道")Layer 3: 相似度 0.887 ← 开始有点不同了("后面好像跟了 money...")Layer 6: 相似度 0.778 ← 差距越来越大Layer 9: 相似度 0.691Layer 12: 相似度 0.614 ← 彻底区分了("这个是银行,那个是河岸")浅层做粗活,深层做细活。一层搞不定的事情,叠几层就能搞定。8.2 多层 = 多步推理有些理解需要想好几步。比如这句话:"The trophy doesn't fit in the suitcase because it is too big"——这里的 "it" 指 trophy 还是 suitcase?浅层:先认出 "it" 是代词,要找它指的是谁中间层:注意到 "too big" 说的是尺寸深层:推理——"太大"所以放不下。放不下的应该是 trophy(要是 suitcase 太大,反而装得下啊)。所以 "it" = trophy一步到位做不出这种推理。每多一层,模型就多一次"想一想、改一改"的机会。层数不是越多越好,但深度确实带来推理能力。 ▍九、完整架构图:把所有积木拼起来到这里,所有零件你都认识了。拼在一起:输入文本: "Thank you very" │ ▼ ─── 分词(第二篇)[10449, 345, 845] ← 3 个 token ID │ ├─▶ Token Embedding(查表) ← 3 个 768 维向量 ├─▶ Position Embedding(查表) ← 3 个位置向量 ▼ ─── 加在一起(第三篇)[向量₁, 向量₂, 向量₃] ← 初始表示 │ │ ┌────────────────────────────────────────┐ │ │ Transformer Layer × 12 │ │ │ │ │ │ LayerNorm → Attention(12头)→ 残差 │ ← 开会 │ │ LayerNorm → FFN(768→3072→768)→ 残差 │ ← 独立思考 │ │ │ │ └────────────────────────────────────────┘ │ ↑ 重复 12 次(GPT-2)/ 32 次(LLaMA 7B) │ ▼ ─── 最终 LayerNorm ▼ ─── LM Head(768 → 50257) ▼ ─── Softmax[..., P("much")=0.992, ...] ← 50257 个概率 │ ▼ ─── 选 token(第一篇)" much"就这些了。没有更多隐藏模块。GPT-4、Claude、LLaMA、DeepSeek——底层全是这个结构。区别只在三个数字:同样的架构,放大规模而已。所以用 GPT-2 理解原理,跟用 GPT-4 完全等价。 ▍十、LM Head 与权重共享架构图最后的 "LM Head(768→50257)" 把向量变回 50257 个概率——模型内部一直在操作向量,这一步翻译回"每个词有多大可能性"。很多模型(包括 GPT-2)中,LM Head 和 Token Embedding 共享同一个权重矩阵——同一张 50257×768 的表,进去的时候按行查向量,出来的时候做矩阵乘法算概率。12 层 Transformer 的任务,就是把最后一个位置的向量推向 "much" 对应的方向。推得越准,概率越高——第一篇看到的 99.2% 就是这么来的。 ▍十一、五篇回顾到这里,前五篇构成了一条完整的链路。大模型从输入到输出的每一步,你都看过了:前五篇回答了"大模型是什么"。 后五篇回答"怎么变得更好":训练(第六篇)、推理优化(第七篇)、上下文窗口(第八篇)、Scaling Law(第九篇)、和 Agent 结合(第十篇)。 ▍十二、结语回头看,Transformer 简单到令人吃惊。总共就两种核心运算——Attention(开会讨论)、FFN(独立思考)。加上 LayerNorm 统一格式、残差连接保留原件,然后重复 N 次。同一个结构,拿去翻译、写代码、做数学、聊天——通通能用。你不需要给每种任务设计不同的模型,只需要把这个简单结构做得足够大。"The unreasonable effectiveness of simple architectures."架构够通用,规模就是最好的武器。下一篇,我们讲这些参数怎么从一堆随机数变成能对话的"大脑"——70 亿个数字,是怎么"学"出来的。本文配套代码:transformer_anatomy.py[1](模型拆解、参数统计、数据流追踪、残差连接验证)。需要 Python 3.8+、transformers、torch。 扫码回复“大模型”获取本系列文章完整配套代码「从零开始理解大模型」是「从零开始理解 Agent」的姊妹系列。Agent 系列讲"四肢",本系列讲"大脑"。建议对照阅读 专栏入口。 相关链接[1] transformer_anatomy.py: cid:link_0
-
欢迎阅读「从零开始理解大模型」系列 —— 十篇文章,从"下一个词预测"到完整的大模型心智模型。每篇配可运行代码。第一篇:一切从"猜下一个词"开始第二篇:Token——大模型眼中的"字"长什么样第三篇:向量与 Embedding——把文字变成数学第四篇:Attention——大模型的"阅读理解"机制(本文)第五篇:Transformer 全景——积木怎么搭成大厦第六篇:训练——70 亿个参数是怎么"学"出来的第七篇:推理——你按下回车后的这一秒发生了什么第八篇:上下文窗口——大模型的"工作记忆"第九篇:Scaling Law——为什么"大力出奇迹"有效第十篇:从大模型到 Agent——下一个词预测如何长出手脚* 本文实操配套代码附件,可在本公众号后台回复“大模型”获取。作者:十一上一篇结尾的链路图中间有一个最大的黑盒:[向量₁', 向量₂', 向量₃'] ← Embedding + 位置编码(第三篇) │ ▼ 进入 Transformer │ ← 这里面发生了什么? ▼ P("much") = 99.2%我们知道 Embedding 给每个词发了一张"特征身份证",但也看到了它的局限——"bank" 不管出现在"I deposited money at the bank"还是"The river bank",查出来的初始向量都一样。模型需要一种机制来读上下文,根据周围的词判断 "bank" 是"银行"还是"河岸",修正每个向量的含义。这个机制就是本篇的主角——Attention(注意力机制)。 ▍一、先说结论一句话总结:Attention 让每个词能"看见"上下文中的所有其他词,并决定该重点关注谁。 ▍二、用"图书馆找书"来理解 Attention想象你走进图书馆,要找关于"量子物理"的信息。你不会把每本书从头到尾读一遍——你会先扫视书架上每本书的标签,大脑自动给它们打分:《量子力学入门》95 分、《经典力学》40 分、《烹饪指南》0 分。然后你把绝大部分精力花在读高分那本书的内容上。Attention 做的就是这个过程,对应三个角色:Query(Q):"量子物理"——你带着的搜索需求Key(K):书架上每本书的标签——用来和你的需求做匹配Value(V):每本书的实际内容——匹配成功后你真正要读的东西完整流程:Q 和每个 K 匹配打分 → 分数高的权重大 → 按权重把所有 V 的内容加起来。回到大模型:模型在处理 "Thank you very ___" 时,最后一个位置(要预测的位置)带着自己的 Query 去"扫视"前面所有词的 Key,发现 "Thank" 和 "very" 的分数最高,于是把它们的 Value 信息重点融合进来——这样 "much" 就获得了最高的预测概率。 ▍三、Q、K、V 怎么来的每个 token 经过 Embedding 后是一个向量(比如 768 维)。Attention 不直接用这个原始向量,而是通过三个权重矩阵,变换出三个不同角色的向量:原始向量 (768维) ├── × W_Q ──→ Query (64维) "我在找什么?" ├── × W_K ──→ Key (64维) "我能提供什么索引?" └── × W_V ──→ Value (64维) "我的实际内容是什么?"为什么要三个不同的向量?因为同一个词在不同角色下需要展示不同的面。比如 "Thank":作为 Query 时它在找和自己搭配的词;作为 Key 时它广播自己是一个感谢用语;作为 Value 时它提供"感谢"的具体语义。W_Q、W_K、W_V 就是权重——训练时调整,推理时固定。第三篇提到的"开源权重中 Attention 占约 49%",主要就是这些矩阵。 ▍四、Attention 的三步计算整个过程可以用一个公式概括:Attention(Q, K, V) = softmax(Q · Kᵀ / √d) · V别被公式吓到——它只是下面三步的缩写。Q · Kᵀ 是第一步(打分),softmax 是第二步(归一化),· V 是第三步(加权求和),√d 是缩放因子防止数值太大。以 "Thank you very" 为例,3 个 token 已经通过 Embedding 变成了向量,现在做 Attention。4.1 第一步:打分(公式中的 Q · Kᵀ / √d)每个词的 Query 和所有词的 Key 做点积,得到相关度分数: Key Thank you very Query Thank [ 0.8 0.2 0.3 ] you [ 0.3 0.5 0.4 ] very [ 0.6 0.2 0.7 ] ← "very" 关注 "Thank" 和自己分数怎么算的? 以 "very" 对 "Thank" 的 0.6 为例:把 "very" 的 Query 向量和 "Thank" 的 Key 向量逐元素相乘再求和(点积):Q(very) · K(Thank) = q₁×k₁ + q₂×k₂ + ... + q₆₄×k₆₄ = 0.6点积越大,说明两个向量方向越一致——Q 要找的和 K 能提供的越匹配。这和第三篇的余弦相似度是同一个直觉。实际模型还会除以 √d(如 √64 = 8),防止点积数值过大导致 Softmax 输出太极端。"打分"到底在打什么分? 这个分数同时承载了三层含义:相似度分。 从向量数学看,点积衡量的是方向一致性。Q(very) 和 K(Thank) 方向接近 → 分数高 → 模型认为当前查询需求和这个位置的信息高度匹配。贡献度分。 这个分数决定了对应的 Value 应该贡献多少信息。分高 → 这个位置的内容对当前上下文重要;分低 → 是"噪声",在后续加权求和时会被过滤掉。注意力分配分。 面对一串 token,模型不可能给所有词同等关注。打分就是在回答:"为了理解当前的词,我应该花多少精力去'看'其他词?"——这就是"注意力"这个名字的由来。就像你在图书馆不会平均阅读每本书,而是把精力集中在最相关的那几本上。4.2 第二步:归一化(公式中的 softmax)把每一行的分数转成概率(加起来等于 1): Key(归一化后) Thank you very Query Thank [ 0.46 0.24 0.30 ] you [ 0.27 0.33 0.40 ] very [ 0.36 0.24 0.40 ] ← 40% 注意力给自己,36% 给 "Thank"怎么算的? 以 "very" 那行 [0.6, 0.2, 0.7] 为例,Softmax 先对每个分数取 e 的指数,再除以总和:e^0.6 = 1.82, e^0.2 = 1.22, e^0.7 = 2.01 总和 = 5.05 归一化: [1.82/5.05, 1.22/5.05, 2.01/5.05] = [0.36, 0.24, 0.40]效果:分数高的被放大,低的被压缩,加起来恰好等于 1——变成了概率分布。这和第一篇预测下一个词时的 Softmax 是同一个操作。4.3 第三步:加权求和(公式中的 · V)用注意力分布作为权重,把所有词的 Value 向量加权求和:"very" 的新向量 = 0.36 × Value(Thank) ← 贡献最大 + 0.24 × Value(you) + 0.40 × Value(very)结果:"very" 的向量里融合了 "Thank" 的信息。当模型用这个新向量预测下一个词时,它"知道"前面是 "Thank you very"——所以 "much" 的概率被大幅提高。Attention 的全部运算就是这三步:打分 → 归一化 → 加权求和。 没有循环,没有条件判断,都是矩阵乘法。 ▍五、实际看 Attention 分数用代码可以提取 GPT-2 真实的 Attention 矩阵:outputs = model(input_ids, output_attentions=True) attention = outputs.attentions[0][0, 0] # Layer 0, Head 0 # attention.shape: [3, 3] ← 3 个 token 之间的注意力分数对 "Thank you very" 的实际结果(文末 attention.py 可以复现):Layer 0, Head 0: Thank you very Thank │ 1.000 · · ← 只能看自己 you │ 0.960 0.040 · ← 96% 注意力给了 "Thank" very │ 0.670 0.180 0.149 ← 67% 注意力给了 "Thank"几个关键观察:"Thank" 那行只有一个 1.000。 它是第一个词,只能看自己(因果掩码),所以 100% 注意力在自己身上。"you" 和 "very" 都重点关注 "Thank"。 这很合理——"Thank" 是这句话的核心语义(感谢),后面的词都需要从它那里获取信息来做出正确预测。右上角全是 ·(零)。 每个词只能看自己和前面的词,不能看后面——这就是因果掩码(Causal Mask)。这就是因果掩码(Causal Mask)——推理时后面的词还没生成,模型不能偷看未来。矩阵右上角全是 0:✓ ✗ ✗ "Thank" 只能看自己 ✓ ✓ ✗ "you" 能看前 1 个词 ✓ ✓ ✓ "very" 能看所有词 ▍六、多头注意力:同时从多个角度看GPT-2 每一层有 12 个注意力头,每个头独立做一套 Q/K/V 计算。为什么要多个头?因为一句话需要从多个角度理解:# 提取所有 12 个头对 "very" 的注意力分布 for head in range(12): att = outputs.attentions[0][0, head, -1, :] # "very" 的注意力 max_token = tokens[att.argmax()] print(f"Head {head}: 最关注 '{max_token}'")不同的头会关注不同的东西——有的头关注语法搭配("very" → "Thank"),有的关注相邻位置("very" → "you"),有的关注自身。12 个头从 12 个角度读同一句话,拼在一起就是全面的理解。没有人告诉 Head 0 去关注语法、Head 3 去关注位置。这些分工是训练过程中自动形成的——每个头学到了一种对预测下一个词有用的关注模式。每个头的 Q/K/V 维度是 64(= 768 / 12),12 个头的结果拼接起来还是 768 维:Multi-Head Attention = Concat(Head₁, Head₂, ..., Head₁₂) × W_O 每个 Headᵢ = Attention(Q_i, K_i, V_i) // 独立计算,64 维 拼接后: 12 × 64 = 768 维 W_O: 输出投影矩阵,768 × 768完整的多头注意力分析代码见附件 multi_head.py。* 本系列文章完整实操配套代码,可在公众号后台回复“大模型”获取。 ▍七、Attention 在完整链路中的位置把第四篇学到的内容嵌入之前的链路:"Thank you very" │ ▼ 分词(第二篇) [10449, 345, 845] │ ▼ Embedding + 位置编码(第三篇) [向量₁, 向量₂, 向量₃] ← 初始向量 │ ▼ Attention(本篇)—— 打开的黑盒 │ 每个向量生成 Q、K、V │ Q · Kᵀ / √d → 打分 │ softmax → 注意力分布 │ 加权求和 V → 融合上下文 │ 12 个头并行,结果拼接 ▼ [新向量₁', 新向量₂', 新向量₃'] ← 融合了上下文 │ ▼ 还有 FFN、残差连接...(第五篇) ▼ 重复 12 层 ▼ → softmax → P("much") = 99.2%但 Attention 只是一层中的一半——另一半是 FFN(前馈网络)和"胶水"组件(残差连接、LayerNorm)。它们一起构成一个完整的 Transformer 层,堆叠几十层才是最终模型。这是下一篇的主题。 ▍八、n² 的代价Attention 的注意力分数矩阵是 n × n(n 为 token 数)。每个词要和所有词算相关度:输入 100 tokens → 100 × 100 = 10,000 次计算 输入 1,000 tokens → 1,000² = 1,000,000 次 输入 10,000 tokens → 10,000² = 100,000,000 次长度增 10 倍,计算量增 100 倍。这就是为什么长文本慢、显存占用大、各种"高效 Attention"变体都在想办法降低 n²。第八篇(上下文窗口)会详细讲。 ▍九、结语Attention 不神秘。它做的事情用一句话就能说清:让每个词看看上下文中的所有词,算出该关注谁,然后把被关注词的信息融合到自己身上。三步运算——打分、归一化、加权求和。多个头并行,从不同角度做同样的事。因果掩码确保只能看过去,不能偷看未来。"Attention is all you need." — Vaswani et al., 20172017 年那篇论文的标题恰如其分。Attention 让每个词能一次性看到整个上下文,这个简单的改变带来了革命性的效果。下一篇,我们把 Attention、FFN、残差连接、LayerNorm 组装起来,看一个完整的 Transformer 层长什么样。本文配套代码:attention.py(Attention 矩阵可视化)、multi_head.py(多头注意力分析)。需要 Python 3.8+、transformers、torch。 扫码回复“大模型”获取本系列文章完整配套代码「从零开始理解大模型」是「从零开始理解 Agent」的姊妹系列。Agent 系列讲"四肢",本系列讲"大脑"。建议对照阅读 专栏入口。
-
近期,开源的AReaL强化学习框架正式发布v1.0版本,提供了较完整的Agentic RL训练底座。围绕这一里程碑版本,华为团队也已完成AReaL在Ascend平台上的支持与增强,不仅补齐了安装与运行能力,还进一步打通了训练、推理、权重同步、Agent场景落地等关键链路,使AReaL在昇腾AI基础软硬件平台上具备更强的工程可用性。近期,开源的Agentic RL框架社区AReaL关注度明显升温。官方仓库在2026年3月2日升级到v1.0,同步引入OpenClaw Agent训练完整样例。此后一周多时间社区Star数暴涨1k+,GitHub当前页面显示仓库已达到约5k Stars。AReaL从“高性能异步RL框架”进一步走向“Agentic RL社区主流底座”的趋势,已经被更多的开发者关注。随着Agent从Demo走向真实业务,行业关注点也正在从“如何让Agent调起工具、跑通流程”,转向“如何让Agent持续学习、自我进化”。在这一过程中,强化学习不再只是模型后训练中的一个可选环节,而是在逐步成为决定Agent上限的关键基础设施。围绕这一阶段,华为团队已完成AReaL在昇腾AI基础软硬件平台上的支持与增强,不仅补齐安装与运行能力,还进一步打通针对大规模MoE模型的训练、推理、权重同步、Agent场景落地等关键链路,使AReaL在昇腾AI基础软硬件平台上从“能训练”进一步走向“可落地、可扩展、可复用”。这意味着,开发者已经可以在昇腾超节点上,基于AReaL运行真实的Agentic RL训练任务,并将其能力扩展到更大模型、更复杂环境以及更长链路的训练场景中。在此基础上,后续将进一步面向真实业务推进落地,持续补齐LoRA RL、On-Policy Distillation RL 等关键能力。 一、安装指南已更新,AReaL v1.0可在昇腾AI基础软硬件平台快速部署AReaL官方近期发布了v1.0稳定版本,标志着其在Agentic RL方向上的框架能力进一步成熟。为方便开发者在昇腾环境中快速部署与验证AReaL v1.0,华为团队已完成其在Ascend NPU平台的安装适配,并同步更新了安装指南。安装文档如下:AReaL Ascend安装指南:https://inclusionai.github.io/AReaL/zh/tutorial/installation_npu.html本次适配围绕基于Ascend的实际训练场景,对相关依赖、运行方式与工程路径进行梳理,使其能够更自然地与vLLM-Ascend、MindSpeed/Megatron、Ray多节点编排等组件协同运行。这意味着:AReaL在Ascend上已经具备可复现、可分享、可推广的基础接入能力。 二、提供完整最佳实践:4个A3节点运行Tau2 Agent训练> 从文档能力走向最佳实践能力很多框架的难点并不在于功能本身,而在于开发者面对真实环境时,缺少一套能够直接复现的配置与步骤。为了让开发者可以真正地“照着就能跑起来”,我们提供了一个完整的昇腾最佳实践样例:cid:link_0该样例面向4个Ascend NPU A3节点,明确给出镜像、容器、环境准备和资源切分方式;在4个节点配置下,文档建议拿出1个节点作为user simulator,并给出推荐的分配模式,具体推荐配置如下:训练场景:Tau2 Agent训练(tau2-airline)模型:Qwen3-30B-A3B硬件规模:4个Ascend NPU A3节点这个样例是AReaL在昇腾上的多节点、分布式、面向真实Agent任务的最佳实践,把昇腾的能力从“理论支持”推进到了“工程最佳实践”。 这传递出一个很清晰的信号:AReaL 在昇腾AI基础软硬件平台上,已经不只是完成了代码级适配,而是具备了面向典型Agent训练任务的端到端运行能力。对于广大开发者开展Agent环境训练、验证RL效果、探索更大规模模型,这一实践都具有较强的参考意义。> 面向真实分布式场景的运行闭环在这个最佳实践中,系统并不是以单机的方式运行,而是完整分布式Agentic RL训练闭环:使用vLLM OpenAI-compatible API server作为用户模拟器服务使用Ray完成4节点集群拉起与调度使用 AReaL负责训练与推理协同使用Megatron/MindSpeed并行模式进行训练侧资源切分其中,推荐的资源分配模式为:allocation_mode: vllm:d4t4+megatron:(attn:d2p4t4|ffn:d1p4e8)这套配置体现了AReaL在昇腾上对推理侧vLLM并行与训练侧Megatron并行的协同支持能力,也体现了其在30B级参数的MoE模型Agent训练场景中的可执行性。 三、与社区Agent训练路径同步:昇腾也可快速接入OpenClaw类RL后训练特别值得强调的是,我们已经与社区主流的Agent训练接入范式保持同步,支持OpenClaw一类Agent框架的强化学习训练接入。AReaL官方README在2026年3月2日新增了OpenClaw示例:cid:link_1给出的表述非常直接:开发者只需要把base_url和api_key替换成AReaL的RL service,就可以训练自己的OpenClaw Agent,不需要复杂依赖,也不需要改代码。其核心机制并不是为某个特定Agent单独适配,而是通过Proxy Gateway提供统一的OpenAI/Anthropic协议兼容入口,使得Agent只需要修改接口地址,就能够接入AReaL的RL训练流水线。这套思路的价值在于,它把原本与具体Agent实现深度耦合的训练接入方式,收敛成了一个更标准化的协议入口。对于使用者来说:不需要修改原有Agent主体逻辑不需要重构业务流程不需要重新设计训练数据链路只需要让Agent的模型请求指向AReaL网关,就可以在原有运行过程中自动采集轨迹,并在获得reward后进入训练闭环。而这一能力在昇腾AI基础软硬件平台上同样可行。这意味着,AReaL在昇腾上的适配,并不局限于基础语言模型RL训练,而是已能够覆盖到更具代表性的Agent场景。这一点非常重要,因为它表明Ascend平台不仅能够支撑“标准RL训练”,还能够承接更贴近下一阶段AI应用形态的Agentic RL训练范式。这为后续更多Agent框架、任务环境和交互式训练流程迁移到昇腾提供了更强信心。 四、AWEX×AReaL:让训练—推理权重同步从“能用”走向“高效、稳定、可规模化”如果说安装指南、Tau2最佳实践和OpenClaw类Agent接入,解决的是“如何跑起来”和“如何快速对接”,那么AWEX×AReaL解决的则是“如何更高效、更稳定地跑起来”。AWEX官方将自己定义为一个面向RL训练—推理权重同步的高性能框架,核心目标是尽可能缩短迭代延迟,让Rollout阶段持续使用最新模型。它强调的几项关键能力包括:只传输必要的shard、支持原地更新、避免冗余复制,并支持NCCL、RDMA、共享内存等多种传输模式。对于大模型、多分片、多节点的RL系统来说,这不是局部优化,而是训练—推理协同效率的关键基础设施。在大模型RL训练系统中,训练与推理之间的权重同步往往是最容易被低估、但又最容易成为瓶颈的一环。尤其在以下场景中,这一问题会迅速放大:模型参数规模进一步增大Dense模型演进到MoE模型并行切分复杂度上升多节点、多卡、多引擎协同运行> AWEX:拓扑感知P2P,只传真正需要的shard针对上述问题,我们在AReaL中完成了AWEX权重同步机制的正式集成。AWEX的核心思路并不是简单替换一种传输方式,而是从拓扑与参数分布角度重新设计权重交换路径:采用拓扑感知的P2P权重交换只传输实际需要的参数shard避免完整权重复制带来的冗余降低显存/bufferfoot print提升大规模场景下的稳定性> AReaL已可通过配置直接启用AWEX本次集成后,AWEX不再是一个独立实验组件,而是已经进入AReaL的使用路径中,开发者可以通过配置直接启用:actor.weight_update_mode: awex由PPOTrainer自动完成相关运行时准备。这意味着对于使用者,AWEX已经从“需要额外手工拼装的能力”变成了“框架内可直接调用的能力选项”。同时,这次工作还补齐了多项框架级支持,使其能够适配更复杂的实际训练部署:从原本主要面向Megatron->SGLang的链路,扩展到Megatron/MindSpeed->vLLM/vLLM-Ascend兼容NPU平台兼容Dense/MoE两类模型补齐训练侧PP模式下的参数meta、layer映射、非均匀pipeline切分支持相关样例与说明也已补齐,包括:AReaL/examples/experimental/awex/README.mdAReaL/examples/experimental/awex/gsm8k_grpo_awex_sample.yamlAReaL/examples/experimental/awex/gsm8k_grpo_awex_npu_sample.yamlAReaL/examples/math/gsm8k_rl.py从工程能力建设角度看,这一步非常关键:AWEX集成AReaL后,权重同步不再是系统中的“额外优化项”,而成为框架主路径中的一部分。这会显著提升后续更大规模RL训练任务在Ascend上的可复用性与推广效率。> 性能收益已经验证:权重同步从“能用”走向“高效、稳定、可规模化”对于基础设施能力来说,是否真正有价值,最终还是要落到数据上。目前,AWEX×AReaL的性能与稳定性收益已经在多个模型规模上得到验证:在qwen3-30B-A3B场景中,4个A3节点上的权重同步时间从约50s降低到约15s在更大的qwen3-235B-A30B场景中,也已能在更低buffer开销下保持稳定运行这些结果说明,AWEX带来的并不只是小规模场景中的局部优化,而是对大模型、多节点、复杂并行RL系统都有效的工程收益。 五、结语:从框架适配走向业务落地,与社区共建昇腾上的Agentic RL能力从安装指南到Tau2最佳实践,从与社区OpenClaw训练接入范式同步,到AWEX高效权重同步进入框架主路径,这一系列工作共同表明:AReaL×Ascend已经在朝着“好用、稳用、规模化可用”的方向持续演进。更重要的是,这些能力并不只服务于框架验证本身,也为后续更大模型、更长上下文、更复杂Agent环境的强化学习训练,打下更坚实的基础。下一阶段,我们将继续面向实际业务场景推进Agentic RL,在昇腾上支持Code Agent、Deep Search Agent、Tool Use Agent等典型Agent的后训练能力,持续打通从训练底座到业务部署的完整链路。我们希望把这项工作做成一个持续演进、可复用、可共建的社区能力。AReaL官方README明确写到,项目欢迎社区贡献,并保持活跃迭代。欢迎对Agentic RL基础设施、Ascend适配、Agent落地和训练系统优化感兴趣的同学,一起加入社区开发,共同把AReaL在昇腾上的能力做深、做稳、做广。AReaL v1.0 的发布,标志着开源 Agentic RL 框架正式迈入成熟可用新阶段。华为云依托在大模型全链路优化领域的深厚技术积淀,在 NPU 异构协同、高效微调训练等方向形成了成熟工程化能力,并将核心优化成果全面贡献至 AReaL 社区。此次基于昇腾平台的深度适配与能力增强,也充分印证:昇腾不仅能高效支撑大模型训练与推理,更可完整承载 Agentic RL 这类更复杂的智能体训练体系。作为推动 Agentic RL 技术落地的重要生态伙伴,华为云不只追求极致的短期性能突破,更致力于构建长期稳定、可持续演进的技术体系,携手社区共同破解大规模分布式训练与推理的工程挑战,为智能体技术规模化落地与行业智能化升级持续注入动能。📌AReaL开源项目:https://inclusionai.github.io/AReaL/zh/tutorial/installation_npu.html 关注AGENT魔方获取更多资讯
-
欢迎阅读「从零开始理解大模型」系列 —— 十篇文章,从"下一个词预测"到完整的大模型心智模型。每篇配可运行代码。第一篇:一切从"猜下一个词"开始第二篇:Token——大模型眼中的"字"长什么样第三篇:向量与 Embedding——把文字变成数学(本文)第四篇:Attention——大模型的"阅读理解"机制第五篇:Transformer 全景——积木怎么搭成大厦第六篇:训练——70 亿个参数是怎么"学"出来的第七篇:推理——你按下回车后的这一秒发生了什么第八篇:上下文窗口——大模型的"工作记忆"第九篇:Scaling Law——为什么"大力出奇迹"有效第十篇:从大模型到 Agent——下一个词预测如何长出手脚* 本文实操配套代码附件,可在本公众号后台回复“大模型”获取。作者:十一上一篇结尾留了一个问题:"France" 和 "Paris" 编号差很远,模型却知道它们有关系?Token ID 只是索引——两个数字之间没有任何语义信息。模型不能直接用整数来计算,它需要一种表示方式,让语义相近的词在数学上也"接近"。这就是向量和 Embedding 的作用。 ▍一、先说结论一句话总结:Embedding 把 token 从"编号"变成"坐标"——在高维空间里,语义相近的词自然聚在一起。 ▍二、Embedding 是什么——给每个词发一张"特征身份证"2.1 为什么 token ID 不够用第二篇我们知道了,每个 token 有一个编号(ID)。但编号只是标签——"猫"是 9246 号,"狗"是 9703 号,"手机"是 11399 号。你问模型"9246 和 9703 像吗?",模型无从判断——编号之间没有语义关系。这就像用身份证号判断两个人是不是亲戚——110101 和 110102 先后办证而已,不代表任何血缘关系。2.2 Embedding 做了什么:把词"画"进坐标系Embedding 不再用编号,而是给每个词分配一组"特征分数"。假设我们只用两个维度——"萌度"和"机械感":猫: [0.9, 0.1] 很萌,不机械狗: [0.8, 0.2] 也很萌,不机械手机:[0.1, 0.9] 不萌,很机械把这些分数画在坐标纸上,"猫"和"狗"自然挨在一起,"手机"离它们很远。距离反映了语义关系——不需要人工标注"猫和狗相似",坐标本身就说明了一切。真实的大模型做的是同样的事,只不过不是 2 个维度,而是 768 个(甚至 4096 个)。每个维度不再是人类能命名的"萌度""机械感",而是模型自己学出来的抽象特征——但效果一样:语义相近的词,坐标接近。2.3 为什么叫"嵌入""Embedding"翻译成"嵌入",名字本身就很形象:把一个孤立的符号,塞进(嵌入)一个充满逻辑关系的空间里。像拼图——每个词是一块拼图,Embedding 找到它在整个世界(高维空间)里最合适的位置。 像找邻居——词一旦被"嵌入",它自动就有了邻居。"国王"的邻居是"女王","苹果"的邻居是"梨"。一句话:Embedding = 把冷冰冰的符号编号,转化成有逻辑、有距离的数字坐标。 ▍三、Embedding 的具体实现:一张查找表理解了"为什么",再看"怎么做"——操作极其简单,就是查表。模型内部有一张表,行数 = 词表大小,列数 = 向量维度。每一行就是一个 token 的向量。embedding_table = model.transformer.wte.weightprint(embedding_table.shape) # torch.Size([50257, 768])50257 行(每个 token 一行),768 列(每个向量 768 维)。当模型看到 "France" 对应的 token ID,它做的事情就是去那一行取出 768 个数字:token_id = tokenizer.encode("France")[0] # 查出 token IDvector = embedding_table[token_id] # 去那一行取向量print(vector.shape) # torch.Size([768])print(vector[:8]) # 前 8 维: [-0.133, 0.098, 0.230, ...]用数学表示:E(token_id) = W_e[token_id] 其中 W_e ∈ ℝ^{V × d}V = 词表大小 (50257)d = 向量维度 (768)没有计算,只有查表。 但这张表里编码了语义——训练过程会把语义相近的词推到空间中相近的位置。参数量感受GPT-2 的 Embedding 表:50257 × 768 ≈ 3860 万个参数,占模型总参数(1.24 亿)的 31%。更大的模型(LLaMA 7B):32000 × 4096 ≈ 1.3 亿个参数——听起来很多,但只占总参数(70 亿)不到 2%。顺便澄清一个常见误解:人们说的"开源权重"(open weights)不是只开源了 Embedding 表。它指的是模型的全部参数——Embedding 只是其中一小部分,大头是后面几十层 Transformer 里的 Attention 权重(~49%)和 FFN 权重(~49%)。 ▍四、语义相似 = 向量接近Embedding 表最神奇的性质是:语义相近的词,向量之间的距离近。度量距离最常用的方式是余弦相似度:cos_sim(A, B) = (A · B) / (‖A‖ × ‖B‖)值域: [-1, 1]1 = 方向完全一致(极其相似)0 = 正交(不相关)-1 = 方向完全相反用代码验证:def cosine_similarity(v1, v2): return torch.dot(v1, v2) / (v1.norm() * v2.norm())france = embedding_table[tokenizer.encode("France")[0]]paris = embedding_table[tokenizer.encode("Paris")[0]]cat = embedding_table[tokenizer.encode("cat")[0]]cosine_similarity(france, paris) # ≈ 0.65 较高,有关系cosine_similarity(france, cat) # ≈ 0.24 低,不相关"France" 和 "Paris" 的余弦相似度远高于 "France" 和 "cat"——模型"知道"法国和巴黎有关系。但没有人告诉它这一点——这个关系是在训练过程中,从几万亿个 token 的统计共现规律中自动涌现的。完整的向量相似度实验代码见附件 embedding.py[1],可以测试任意词对的相似度。 ▍五、向量算术:"国王 - 男人 + 女人 ≈ 女王"2013 年 Word2Vec 发现了一个惊人的现象——向量运算能反映语义关系:E("king") - E("man") + E("woman") ≈ E("queen")从"国王"的概念里减去"男性"成分,加上"女性"成分,得到的向量最接近"女王"。这意味着向量空间里存在有意义的方向。从 "man" 到 "woman" 的方向,和从 "king" 到 "queen" 的方向是平行的——这个方向编码了"性别"这一语义关系。类似的:E("Paris") - E("France") + E("Germany") ≈ E("Berlin") // 国家→首都E("walked") - E("walk") + E("run") ≈ E("ran") // 动词时态这些关系不是人工编码的——它们是"预测下一个词"的训练副产品。 模型为了更准确地预测,自动发现了词与词之间的语义关系,并把它们编码为向量空间中的几何结构。不过要注意:GPT-2 的 Embedding 表只是初始向量,类比推理的准确率有限。真正强大的语义表示在 Transformer 的深层——这是第四篇(Attention)的主题。完整的向量算术实验代码见附件 analogy.py[2],支持命令行输入任意三个词做类比。 ▍六、维度的含义与降维可视化768 个维度,每一个代表什么?答案是:没有人类可理解的含义。 和 GPS 的"经度=东西、纬度=南北"不同,Embedding 的每个维度都是抽象的、纠缠的——单独看一个维度没意义,只有所有维度组合起来才编码了语义。但我们可以用降维算法(如 PCA)把 768 维压缩到 2 维来可视化。用代码对几十个词做 PCA:# 收集"国家、城市、动物、颜色"四组词的 Embedding 向量# PCA 降到 2 维# 结果:同一组的词聚在一起!即使暴力压缩到 2 维,语义聚类依然清晰:国家挤在一起、动物挤在一起、颜色挤在一起。这说明 Embedding 空间的语义结构是高度有组织的。完整的降维可视化代码见附件 visualize.py[3],输出终端散点图和 JSON 数据文件。 ▍七、Embedding 的局限:一个词只有一个向量?到目前为止我们看到的都是 Embedding 表里的初始向量——每个 token 有一个固定的向量,不管上下文是什么。但同一个词 "bank" 在不同语境里意思完全不同:"I deposited money at the bank" → 银行"The river bank was covered" → 河岸问题来了:两句话里的 "bank" 查的是 Embedding 表的同一行,取出来的向量完全相同。一个固定的向量,怎么表达两个不同的意思?答案是:Embedding 只是起点,不是终点。模型拿到初始向量后,会经过一系列变换——下一篇要讲的 Attention 机制就是核心。你可以把这个过程想象成"读完整句话后修正对每个词的理解":看到 "deposited money at the bank",模型把 "bank" 的向量往"金融机构"方向推看到 "river bank was covered",模型把 "bank" 的向量往"河岸地形"方向推起点相同,终点不同。 同一个初始向量,经过不同上下文的"修正"后,变成了截然不同的向量。用代码实际测量(附件 context_embedding.py[4])可以看到:刚从 Embedding 表取出来时,两个 "bank" 的向量完全一样(相似度 1.0);经过模型的全部变换后,相似度降到了约 0.6——它们已经被区分开了。这里有一个重要的分工,理解它是理解后续章节的关键:Embedding 表负责"你是谁"——token 的基本身份Transformer(第四、五篇)负责"你在这里是什么意思"——结合上下文的理解Embedding 给了一个粗略的初始猜测,Transformer 根据上下文把它精修为准确的语义表示。下一篇讲的 Attention,就是 Transformer 用来"读上下文"的核心机制。 ▍八、位置编码:顺序也得编进向量里还有一个问题:Embedding 表根据 token ID 查出向量,但它不知道 token 在句子里的位置。"狗 咬 人" 和 "人 咬 狗" 包含完全相同的三个 token,如果只看 Embedding,三个向量完全一样——模型无法区分这两句话。解决方法是位置编码(Positional Encoding)——给每个位置也分配一个向量,然后加到 token 向量上:最终输入向量 = Token_Embedding(token_id) + Position_Embedding(position)其中 position 就是 token 在句子中的位置序号(从 0 开始)以 "Thank you very" 为例:位置 0 的 "Thank": Token_Embedding(10449) + Position_Embedding(0)位置 1 的 "you": Token_Embedding(345) + Position_Embedding(1)位置 2 的 "very": Token_Embedding(845) + Position_Embedding(2)同样的词出现在不同位置,加上的位置向量不同,最终向量就不同——模型因此能区分 "狗咬人" 和 "人咬狗"。GPT-2 用的是可学习的位置编码——和 Token Embedding 一样,每个位置有一个 768 维向量,训练过程中自动调整:position_table = model.transformer.wpe.weightprint(position_table.shape) # torch.Size([1024, 768])# 1024 个位置,每个位置 768 维 → GPT-2 最多处理 1024 个 token更新一代的模型(LLaMA、Qwen)用的是 RoPE(旋转位置编码),原理不同但目标一样——让模型知道 token 的先后顺序。RoPE 的细节在第八篇(上下文窗口)展开。 ▍九、完整链路:三篇拼起来三篇文章读完,输入端的完整流程你已经清楚了:"Thank you very" │ ▼ 分词(第二篇)[10449, 345, 845] ← 3 个 token ID │ ▼ Token Embedding 查表(本篇)[向量₁, 向量₂, 向量₃] ← 3 个 768 维向量 │ ▼ + Position Embedding(本篇)[向量₁', 向量₂', 向量₃'] ← 加了位置信息 │ ▼ 进入 Transformer(第四、五篇) │ ← 这里面发生了什么? ▼[概率₁, 概率₂, ..., 概率₅₀₂₅₇] ← P("much") = 99.2%下一篇,我们打开中间那个最大的黑盒——Transformer。它的核心机制叫 Attention,解决的问题是:每个词怎么"看到"上下文中的其他词? ▍十、结语Embedding 做的事情简单到极致——查表。但这张表里编码了人类语言的语义结构。训练后,五万多个 token 在 768 维空间里各就各位:国家和国家在一起,动物和动物在一起,"国王 - 男人 + 女人 ≈ 女王"。这个结构不是人工设计的,是从"预测下一个词"这个单一任务中涌现的。"You shall know a word by the company it keeps." — J.R. Firth, 1957"一个词的意思,由它的邻居决定。"这句 1957 年的语言学格言,恰好是 Embedding 的最好注脚——向量空间里的每一个位置,都是统计共现关系的结晶。 本文配套代码:embedding.py[1](向量相似度)、analogy.py[2](向量算术)、visualize.py[3](降维可视化)、context_embedding.py[4](初始 vs 上下文向量)。需要 Python 3.8+、transformers、torch、numpy。 扫码回复“大模型”获取本系列文章配套代码(持续更新)「从零开始理解大模型」是「从零开始理解 Agent」的姊妹系列。Agent 系列讲"四肢",本系列讲"大脑"。建议对照阅读。相关链接[1] embedding.py: cid:link_1[2] analogy.py: cid:link_3[3] visualize.py: cid:link_2[4] context_embedding.py: cid:link_0
-
欢迎阅读「从零开始理解大模型」系列 —— 本系列十篇文章,从"下一个词预测"到完整的大模型心智模型。每篇配可运行代码。第一篇:一切从"猜下一个词"开始第二篇:Token——大模型眼中的"字"长什么样(本文)第三篇:向量与 Embedding——把文字变成数学第四篇:Attention——大模型的"阅读理解"机制第五篇:Transformer 全景——积木怎么搭成大厦第六篇:训练——70 亿个参数是怎么"学"出来的第七篇:推理——你按下回车后的这一秒发生了什么第八篇:上下文窗口——大模型的"工作记忆"第九篇:Scaling Law——为什么"大力出奇迹"有效第十篇:从大模型到 Agent——下一个词预测如何长出手脚* 本文实操配套代码附件,可在本公众号后台回复“大模型”获取。作者:十一上一篇我们看到,模型以 99.2% 的概率预测 "Thank you very" 后面接 "much"。代码里有一行被跳过了:input_ids = tokenizer.encode("Thank you very", return_tensors="pt")# 输出: [10449, 345, 845]三个英文单词变成了三个数字。模型看到的不是 "Thank you very",而是 [10449, 345, 845]。这三个数字就是三个 token。 Token 是大模型的最小阅读单位——模型不认识字母,不认识汉字,它只认识 token。本篇回答三个问题:token 到底是什么?怎么切出来的?为什么你需要关心它?▍一、先说结论一句话总结:Token 是人类语言和模型之间的翻译层——人类写字,模型读 token。 ▍二、Token 不是"词"用第一篇的 tokenizer 来看几个例子:tokenizer.encode("Hello") # → [15496] 1 个 tokentokenizer.encode("hello") # → [31373] 1 个 token,但 ID 不同!tokenizer.encode("Kubernetes") # → [42, 18478, 3262, 274] 4 个 token,被切碎了tokenizer.encode("strawberry") # → [301, 1831, 8396] 3 个 tokentokenizer.encode("你好") # → [19526, 254, 25001, 121] 4 个 token!几个反直觉的事实:大小写不同 = 不同的 token。 "Hello"(15496)和 "hello"(31373)在模型眼里是两个完全不同的"字"。空格被编进 token 里。" capital"(前面带空格)和 "capital" 是不同的 token。GPT 系列把空格粘在下一个词的前面。专业术语被切碎。 "Kubernetes" 不在词表里,被切成了四段碎片(K-uber-net-es)。模型需要从碎片中"拼"出含义。中文被切得更碎。 两个汉字 "你好" 竟然变成了 4 个 token——因为 GPT-2 的词表主要基于英文构建,中文字符被拆成了字节级碎片。完整的分词实验代码见附件 tokenizer_demo.py[1],支持命令行输入任意文本测试。 ▍三、BPE——Token 是怎么"切"出来的为什么 "Hello" 是 1 个 token 而 "Kubernetes" 是 4 个?这由一个叫 BPE(Byte Pair Encoding,字节对编码) 的算法决定。3.1 核心思路你需要设计一个固定大小的"字典"(比如 50257 个 token),用来表示所有可能的文本。极端方案一:收录所有词 → 词无穷多,不可行。 极端方案二:只收 256 个字节 → 任何文本都能表示,但效率极低。BPE 的做法是:从单个字节开始,反复合并最频繁的相邻对,直到词表达到目标大小。 形式化表示:初始词表 V₀ = {所有单字节} // 256 个重复以下步骤: (a, b) = argmax_{所有相邻对} count(a, b) // 找出语料中出现次数最多的相邻 token 对 合并 a + b → ab // 创建新 token V_{i+1} = V_i ∪ {ab} // 加入词表直到 |V| = 目标大小(如 50257)3.2 一个具体例子训练语料:low low low low low lower lower newest newest newest widest起点:每个字符是一个 token → {l, o, w, e, r, n, s, t, i, d}轮次 1: (l, o) 出现 7 次 → 合并为 "lo"轮次 2: (lo, w) 出现 7 次 → 合并为 "low"轮次 3: (e, s) 出现 4 次 → 合并为 "es"轮次 4: (es, t) 出现 4 次 → 合并为 "est"轮次 5: (n, e) 出现 3 次 → 合并为 "ne"轮次 6: (ne, w) 出现 3 次 → 合并为 "new"轮次 7: (new, est) 出现 3 次 → 合并为 "newest"...训练完成后,用这些合并规则对新词分词:"low" → [low] 训练中高频,完整保留"lowest" → [low, est] 两个已知子词拼接("lowest" 训练时没见过!)"newest" → [newest] 训练中高频,完整保留"widest" → [w, i, d, est] 低频词被切碎,但 "est" 被识别出来3.3 BPE 的三个关键性质纯统计过程。 BPE 不懂语言学。"est" 被合并不是因为算法知道它是后缀,而是因为它在语料中频繁出现。结果恰好和语言学吻合——这是统计的副产品。高频完整、低频切碎。 训练数据中出现越多的片段越容易被合并成完整 token。这就是 "Hello" 是 1 个 token 而 "Kubernetes" 是 4 个的原因——前者在互联网文本中远比后者常见。能泛化到新词。 "lowest" 没出现在训练语料中,但 "low" 和 "est" 都是已学到的 token,新词自动被切成 [low, est]。BPE 天然处理了"未见过的词"。关键洞察:BPE 词表是在训练数据上学出来的。训练数据以英文为主 → 英文词表丰富、切得粗、效率高;中文作为"非主流语言" → 词表位置少、切得碎、效率低。这不是歧视,是统计分布的自然结果。完整的 BPE 手写实现见附件 bpe_demo.py[2],纯 Python 无依赖,可以修改训练语料观察词表变化。 ▍四、Token 效率——为什么你需要关心Token 数量直接决定三件事:你花多少钱、等多久、能塞多少内容。4.1 中英文效率对比同一句话,中文版的 token 数通常是英文版的 2-3 倍(GPT-2 分词器):4.2 三个影响公式API 费用 = token_count × price_per_token生成延迟 ≈ output_tokens × time_per_token可用上下文 ≈ context_window / avg_tokens_per_char同样的内容用中文处理,费用翻倍、速度减半、能塞进上下文的内容更少。这就是为什么 DeepSeek、Qwen 等国产模型专门优化了中文词表:趋势明确:新一代模型都在扩大词表、增加多语言 token。 词表大了,常见中文词可以作为完整 token 存在,不需要被切成字节碎片。完整的中英文效率对比和费用估算代码见附件 token_cost.py[3]。 ▍五、"strawberry 里有几个 r"——分词决定模型的视力2024 年有一个著名的测试:问大模型 "How many r's in strawberry?",很多模型答错。原因不在"智力",而在"视力":tokenizer.encode("strawberry") # → ["st", "raw", "berry"]模型看到的是三个 token,看不到单独的 "r"。要数 "r" 的数量,它需要在 token 内部做字符级推理——而这不是它被训练来做的事。Token 是模型能看到的最小单位。 Token 内部的字符结构对模型来说是模糊的。很多"能力缺陷"——数不对字母、处理不好罕见语言、无法做字符级操作——根源都在分词层。模型的智力受限于它的视力。 ▍六、特殊 Token:模型的控制信号除了表示文本的普通 token,还有一类特殊 token——不对应自然语言,用来给模型发"指令":<|endoftext|> → 文本结束<|system|> → 系统消息<|user|> → 用户消息<|assistant|> → "现在该我说了"第一篇提到,当你问 "法国的首都是哪里?" 时,模型看到的输入其实是:<|system|>你是一个有帮助的助手。<|end|><|user|>法国的首都是哪里?<|end|><|assistant|>这些特殊 token 不来自 BPE,而是手动添加到词表中的。如果你读过 Agent 系列第一篇,Agent 的 messages 列表中的 role 字段,发送给模型前就会被转换成这些特殊 token。JSON 是给人看的,特殊 token 才是给模型看的。模型生成 <|endoftext|> 时就意味着"我说完了"——这就是 Agent 循环中"任务结束"的底层信号。 ▍七、Token 在完整链路中的位置回顾第一篇的代码,现在你对每一步的理解都深了一层:# 第一步:分词 —— 本篇的主题input_ids = tokenizer.encode("Thank you very", return_tensors="pt")# "Thank you very" → [10449, 345, 845] ← 3 个 token ID# 第二步:token ID → 向量(查 Embedding 表)→ 第三篇# [10449, 345, 845] → 3 个 768 维的向量# 第三步:向量经过 Attention 层层变换 → 第四、五篇# 3 个向量 → 经过 12 层 Transformer → 3 个新向量# 第四步:最后一个向量 → 50257 个概率 → 第一篇已讲过# softmax(linear(最后一个向量)) → P("much") = 99.2%Token 是这条链路的起点。它决定了模型看到什么、看不到什么、用户花多少钱、模型跑多快、上下文能装多少。下一篇,我们打开第二个黑盒:token ID 是怎么变成向量的?为什么 "France"(ID 4881)和 "Paris"(ID 6342)编号差很远,模型却知道它们有关系? ▍八、结语Token 是大模型和人类语言之间的翻译层。这个翻译层看起来不起眼,但它决定了模型的视力边界、使用成本和处理效率。"数不对字母、中文比英文贵、长文本容易截断"——追到底,都能在 token 这一层找到原因。"Between the human world and the model world, there is a thin layer of translation. That layer is tokenization."理解了 token,你就理解了大模型的"入口"。本文配套代码:tokenizer_demo.py[1](分词实验)、bpe_demo.py[2](手写 BPE)、token_cost.py[3](效率对比)。需要 Python 3.8+、transformers。 扫码回复“大模型”获取本系列文章配套代码(持续更新)「从零开始理解大模型」是「从零开始理解 Agent」的姊妹系列。Agent 系列讲"四肢",本系列讲"大脑"。建议对照阅读。 相关链接[1] tokenizer_demo.py: cid:link_0[2] bpe_demo.py: cid:link_2[3] token_cost.py: cid:link_1
-
欢迎阅读「从零开始理解大模型」系列 —— 本系列十篇文章,从"下一个词预测"到完整的大模型心智模型。每篇配可运行代码。第一篇:一切从"猜下一个词"开始(本文)第二篇:Token——大模型眼中的"字"长什么样第三篇:向量与 Embedding——把文字变成数学第四篇:Attention——大模型的"阅读理解"机制第五篇:Transformer 全景——积木怎么搭成大厦第六篇:训练——70 亿个参数是怎么"学"出来的第七篇:推理——你按下回车后的这一秒发生了什么第八篇:上下文窗口——大模型的"工作记忆"第九篇:Scaling Law——为什么"大力出奇迹"有效第十篇:从大模型到 Agent——下一个词预测如何长出手脚作者:十一你每天都在用大模型——写代码、问问题、翻译文章。但你有没有想过:它到底在"做"什么?答案简单到令人意外:猜下一个词。不是"理解",不是"思考",不是"推理"——就是给定前面的文字,预测下一个最可能出现的词。ChatGPT、Claude、DeepSeek,所有大模型的底层任务都是同一个:Next Token Prediction(下一个词预测)。▍一、先说结论一句话总结:大模型 = 一个把"猜下一个词"做到极致的概率机器。 ▍二、你可以亲手体验的"下一个词预测"打开手机输入法,输入"今天天气",看看联想栏——它会建议"真好""不错""怎么样"。这就是下一个词预测。输入法和大模型做的事情本质上相同,区别只在规模:大模型是把输入法的思路推到了极致。参考信息更多、模型更大、预测更准,于是从"联想下一个词"变成了"生成完整文章"。 ▍三、代码实验一:亲眼看"下一个词预测"用 GPT-2(1.2 亿参数的小模型,和 GPT-4 架构完全相同)来演示。核心代码只有几行:input_ids = tokenizer.encode("Thank you very", return_tensors="pt") # 文字 → token IDlogits = model(input_ids).logits[0, -1, :] # 模型输出 50257 个原始得分probs = torch.softmax(logits, dim=0) # 得分 → 概率next_token = torch.argmax(probs) # 选概率最高的完整可运行代码见附件 predict.py[1],支持命令行传入任意文本。* 本公众号AGENT魔方后台回复“大模型”,获取配套代码(持续更新)运行结果输入: 'Thank you very'Token IDs: [10449, 345, 845]模型预测后面最可能的 10 个词:-------------------------------------------------- 1. ' much' 99.2% █████████████████████████████████████████████████ 2. ' very' 0.3% 3. ',' 0.3% 4. 'much' 0.1% ... 剩余 50247 个 token 共占: 0.1%拼接后: 'Thank you very much'99.2%——50257 个候选词里,模型把几乎全部概率都押在了 "much" 上。50257 是怎么来的? 这是 GPT-2 的词表大小——模型认识的所有 "词"(准确说是 token)的数量。它通过 BPE 算法(第二篇详解)从训练数据中统计出来:50000 个高频子词 + 256 个基础字节 + 1 个特殊结束符 = 50257。不同模型的词表大小不同(LLaMA 是 32000,Qwen2 是 151936),但原理相同。模型每做一次预测,就是在自己的词表里给这些 token 逐个打分。Softmax:得分变概率模型输出的不是概率,而是 50257 个原始得分(叫 logits)。Softmax 函数把它们转换成加起来等于 1 的概率分布:P(wᵢ) = e^(logitsᵢ) / Σⱼ e^(logitsⱼ)效果是:得分高的被放大,得分低的被压缩。 "much" 的 logit 远高于其他词,Softmax 之后变成了 99.2% 的压倒性概率。这个函数在大模型里反复出现(Attention 的归一化也用它),后面的篇章会多次遇到。试试别的输入:python predict.py "The capital of France is"——GPT-2 对这个问题没这么自信,"Paris" 只排第 5 位、概率约 3%。模型越大、预测越准——这是第九篇(Scaling Law)的主题。 ▍四、代码实验二:逐词生成与 Temperature上面只预测了一个词。把过程循环起来,就是完整的文本生成:for step in range(max_tokens): logits = model(input_ids).logits[0, -1, :] probs = torch.softmax(logits / temperature, dim=0) # 先除以 T,再 Softmax next_token = torch.multinomial(probs, 1) # 按概率随机采样 input_ids = torch.cat([input_ids, next_token], dim=1) # 拼接,继续下一轮Temperature T 的作用写在公式里一目了然:P(wᵢ) = e^(logitsᵢ / T) / Σⱼ e^(logitsⱼ / T)完整可运行代码见附件 generate.py[2],默认跑三组 Temperature 对比实验。* 本公众号AGENT魔方后台回复“大模型”,获取配套代码(持续更新)运行结果示例Temperature = 0.3(保守): 'The meaning of life is to be found in the way we live.' ← 每步选的词概率 10%-30%,几乎每次结果一样Temperature = 1.5(奔放): 'The meaning of life is pretty complicated, especially when you realize how small we are.' ← 每步选的词概率不到 1%,每次结果不同生成 = 循环预测。 你在 ChatGPT 里看到的"打字机效果"不是动画——那就是模型真实的工作方式,一个 token 一个 token 地生成。每步都是同一个操作:算 logits → Softmax → 选 token → 拼上去 → 继续。 ▍五、一个颠覆直觉的事实预测下一个词就能写文章、做翻译、解数学题?是的。当你把"预测"做到足够好,很多能力会自动涌现:翻译:输入 "Translate to French: The cat is on the mat →",模型预测 "Le"、"chat"……在预测中完成了翻译。数学:输入 "127 + 385 =",模型预测 "5"、"1"、"2"——在预测中完成了计算。代码:输入 "def sort_list(",模型依次预测参数、函数体……这些能力不是专门训练出来的——它们是"预测下一个词"的副产品。 模型在训练时读过大量翻译文本、数学题、代码,"学"到了:在特定格式的输入后面应该接什么。这个洞察是理解整个系列的地基:大模型的一切能力,都是"预测下一个词"在不同场景下的表现。 ▍六、从"预测"到"对话"你问大模型问题时,它明明在"回答"——这和"预测下一个词"有什么关系?看看模型实际看到的输入:<|system|>你是一个有帮助的助手。<|end|><|user|>法国的首都是哪里?<|end|><|assistant|>模型的任务就是预测<|assistant|>后面应该接什么词:"法"、"国"、"的"、"首"、"都"、"是"、"巴"、"黎"、"。""回答问题"不是一种独立的能力。 它只是"预测下一个词"在对话格式下的表现。模型训练时见过海量"问 → 答"文本,所以看到问题格式的输入时,预测出来的词自然构成了回答。如果你读过「从零开始理解 Agent」系列,你会记得 Agent 的核心是一个循环——LLM 预测 → 工具调用 → 结果反馈 → LLM 继续预测。Agent 的每一个决策——用哪个工具、传什么参数、什么时候停止——都是一次"猜下一个词"。 ▍七、所有大模型都一样吗?GPT-4、Claude、DeepSeek、Llama……底层真的都是"预测下一个词"?是的。差别不在"做什么",而在"做得多好":但底层任务完全相同:给定前文,预测下一个 token。这就像所有汽车都是"燃烧燃料推动活塞"——但法拉利和五菱宏光体验天差地别,差别在工程细节。 ▍八、这个系列要讲什么这篇建立了最核心的直觉:大模型 = 预测下一个词。但留下了很多问题:"文字怎么变成数字?" —— tokenizer.encode 做了什么?→ 第二篇:Token"模型内部怎么算的?" —— model(input_ids) 里发生了什么?50257 个 logits 怎么来的?→ 第三~五篇:向量、Attention、Transformer"参数怎么学出来的?" —— 模型凭什么知道 "Thank you very" 后面接 "much"?→ 第六篇:训练"为什么生成有快有慢?" —— 第一个 token 等得久?KV Cache?→ 第七篇:推理每篇解开一个黑盒,到第十篇结束时,你会拥有大模型"从输入到输出"的完整心智模型。 ▍九、结语大模型不神秘。它做的事情和你手机输入法一模一样——预测下一个词。只不过,当这个"预测"被推到极致——参数够多、数据够大、训练够久——涌现出来的东西,看起来像是在"思考"。All models are wrong, but some are useful. — George E. P. Box所有模型都是"错"的——它不是真的在理解语言。但足够好的预测,就是足够好的近似。下一篇,我们打开第一个黑盒:模型看到的不是"文字",而是"token"。本文配套代码:predict.py[1](单步预测)、generate.py[2](逐词生成 + Temperature 对比)。需要 Python 3.8+、transformers、torch。 公众号后台回复“大模型”获取本系列文章配套代码(持续更新)「从零开始理解大模型」是「从零开始理解 Agent」的姊妹系列。Agent 系列讲"四肢"——怎么通过工具和循环做事。本系列讲"大脑"——内部怎么运转。建议对照阅读。 相关链接 [1] predict.py: cid:link_1[2] generate.py: cid:link_0
-
「从零开始理解 Agent」系列番外 —— 在前面的七篇正文里(文末超链接直达),我们从来没关心过一个问题:跑一次 Agent 到底消耗多少 Token?每轮循环花了多少?工具返回结果占了多大比例?这篇番外给 Agent 装上一个 Token 仪表盘,让消耗一目了然。作者:十一▍一、为什么要关心 Token?用 Agent 和用普通对话最大的成本差异在于:对话是一问一答,Agent 是一个循环。一次普通对话:1 次 API 调用,消耗一份 Token。一次 Agent 任务:可能调用 5-15 次 API,每次调用都带着完整的 messages 历史,而且 messages 每轮都在增长——每调用一次工具,messages 至少新增两条(LLM 的回复 + 工具返回结果)。输入 Token 是累积增长的,不是线性增长的。具体消耗多少,取决于工具返回结果的长度——ls 返回几行和 cat 一个千行文件,差距可以是几十倍。所以 Token 消耗不能靠估算,要靠实际测量。▍二、API 返回的 usage 字段好消息是,OpenAI 兼容的 API 每次调用都会返回 Token 使用情况:response = client.chat.completions.create( model=MODEL, messages=messages, tools=TOOLS)# response.usage 包含这三个字段:# - prompt_tokens: 输入 Token 数(messages + tools schema)# - completion_tokens: 输出 Token 数(LLM 的回复)# - total_tokens: 两者之和我们只需要在每轮循环中把这个数据收集起来。▍三、给 Agent 加一个 Token 追踪器在第一篇的 agent.py 基础上,只需要加一个简单的数据结构:class TokenTracker: """追踪 Agent 整个生命周期的 Token 消耗""" def __init__(self): self.rounds = [] # 每轮的详细数据 self.total_input = 0 self.total_output = 0 def record(self, round_num, usage, message_count): """记录一轮循环的 Token 消耗""" input_tokens = usage.prompt_tokens output_tokens = usage.completion_tokens self.rounds.append({ "round": round_num, "input": input_tokens, "output": output_tokens, "total": input_tokens + output_tokens, "messages": message_count }) self.total_input += input_tokens self.total_output += output_tokens def summary(self): """打印消耗摘要""" print(f"\n{'='*50}") print(f"Token 消耗统计") print(f"{'='*50}") print(f"{'轮次':<6} {'输入':>8} {'输出':>8} {'合计':>8} {'消息数':>6}") print(f"{'-'*50}") for r in self.rounds: print(f"{r['round']:<6} {r['input']:>8} {r['output']:>8} " f"{r['total']:>8} {r['messages']:>6}") print(f"{'-'*50}") print(f"{'合计':<6} {self.total_input:>8} {self.total_output:>8} " f"{self.total_input + self.total_output:>8}") print(f"{'='*50}")嵌入 Agent 循环:def run_agent(user_message, max_iterations=10): tracker = TokenTracker() messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_message} ] for i in range(max_iterations): response = client.chat.completions.create( model=MODEL, messages=messages, tools=TOOLS ) message = response.choices[0].message # 记录本轮消耗 tracker.record(i + 1, response.usage, len(messages)) ifnot message.tool_calls: tracker.summary() # 任务结束时打印统计 return message.content # 执行工具调用... messages.append(message) for tool_call in message.tool_calls: result = execute_tool(tool_call) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result }) tracker.summary() return"Max iterations reached"▍四、实际输出长什么样需要说明的是,不同任务的 Token 消耗差异很大——一个"创建 hello.py"可能 2 轮就结束,一个"重构整个项目"可能跑 20 轮。下面的数据只是一个具体案例,目的是让大家对 Agent 的 Token 消耗有个感性认识,而不是一个通用基准。让 Agent 执行"找到当前目录的 Python 文件,统计行数,写入报告",Token 追踪器的输出类似这样(以下为示意数据,实际数值因模型和任务而异):==================================================Token 消耗统计==================================================轮次 输入 输出 合计 消息数--------------------------------------------------1 523 87 610 22 1204 103 1307 53 2891 76 2967 84 3542 45 3587 115 3870 156 4026 13--------------------------------------------------合计 12030 467 12497==================================================几个一眼就能看出的规律:输入 Token 逐轮递增。 第 1 轮 523,第 5 轮 3870——因为每轮都要把完整的 messages 历史发给 LLM,历史越长输入越大。输出 Token 相对稳定。 每轮只有几十到一两百——LLM 的回复通常就是一段思考 + 一次工具调用的 JSON。输入远大于输出。 在这个示例中,输入占了总消耗的绝大部分。这在 Agent 场景中是普遍规律——意味着降低成本的关键是控制输入,不是控制输出。▍五、Token 都花在哪了?输入 Token 可以拆成三部分:输入 Token = system prompt(含 Skills)+ tools schema + 历史 messages其中:system prompt 每轮都要带,固定成本。如果只有基础指令,通常几百 Token。但回忆第三篇——Rules 和 Skills 的内容都是注入到 system prompt 中的。一旦挂载了几个 Skill(每个 Skill 的描述可能几百到上千 Token),system prompt 就会从几百膨胀到几千甚至上万。这是一个容易被忽略的固定开销:每一轮 API 调用都要重复发送全部 Skill 描述。tools schema 也是每轮都要带。nanoAgent 的三个工具(read_file、write_file、execute_bash),JSON Schema 大约几百 Token(具体取决于参数描述的详细程度)。但这是最简情况。生产级 Agent 动辄注册十几个甚至几十个工具,每个工具的参数描述、枚举值、嵌套结构都会占 Token。工具数量增长十倍,tools schema 的开销也会相应增长——而且这个成本每轮都要付。这也是为什么第三篇的 MCP 动态加载工具、而不是把所有工具都塞进去的原因之一:按需加载,用不到的工具不注册,省的是每一轮的固定税。历史 messages 这是大头,也是唯一会增长的部分。增长速度取决于工具返回结果的长度——ls 返回几行,cat 一个大文件可能返回几千行。总结一下:system prompt(含 Skills)和 tools schema 是"固定税",每轮都交;历史 messages 是"累进税",越跑越多。降本要两手抓——减少固定税(精简 Skill 数量与描述、精简工具)和控制累进税(截断输出、及时压缩)。▍六、和第六篇压缩的关系现在回头看第六篇的上下文压缩,它做的事情就清楚了:砍掉历史 messages 中的旧内容,降低每轮的输入 Token。没有压缩时,Token 消耗曲线是这样的:输入 Token ^ | / | / | / ← 越来越贵 | / | / |/ +------------→ 轮次有压缩时:输入 Token ^ | /\ /\ | / \/ \ ← 锯齿形,有上限 | / | / |/ +------------→ 轮次压缩把一条单调递增的曲线变成了有上限的锯齿波。Token 追踪器加上压缩,你就能精确看到每次压缩省了多少 Token。在 TokenTracker 中加一行标记压缩事件:def record_compaction(self, round_num, before_tokens, after_tokens): """记录一次压缩事件""" saved = before_tokens - after_tokens print(f" [压缩] 轮次 {round_num}: {before_tokens} → {after_tokens} " f"(节省 {saved} tokens, {saved/before_tokens*100:.0f}%)")▍七、几条实用的成本控制经验有了 Token 追踪器之后,一些优化方向会变得很直观:截断工具输出。 第七篇安全篇里已经做了输出截断(MAX_OUTPUT_LENGTH),它不只是为了安全,也是成本控制的第一道防线。cat 一个 10000 行的文件会让后续每一轮都多带 10000 行的历史——截断到前 200 行,后续每轮都能省下大量输入 Token。减少不必要的工具调用。 有时 LLM 会先 ls 看一下目录,再 cat 某个文件,再 grep 搜索内容——而实际上一条 grep -r "keyword" . 就能搞定。更好的 system prompt 可以引导 LLM 用更少的步骤完成任务。清理不用的 MCP 和 Skill。 第五节讲了,tools schema 和 Skill 描述是每轮都要付的"固定税"。注册了 10 个 MCP 工具但日常只用 3 个,剩下 7 个的 schema 每轮都在白白消耗 Token。Skill 同理——挂载了五个 Skill 但当前任务只涉及其中一个,其余四个的描述都是浪费。定期审视已注册的 MCP 和 Skill,删掉不用的,是最简单的降本手段。选对模型。 简单任务(文件操作、格式转换)用便宜的小模型,复杂任务(代码重构、架构分析)用贵的大模型。这就是为什么有些 Agent 框架支持"模型路由"——根据任务复杂度自动选模型。及时压缩。 第六篇的压缩阈值不要设太高。阈值越高,压缩前的几轮输入 Token 越大。根据 Token 追踪器的数据调整阈值,找到"压缩频率"和"摘要质量"之间的平衡点。▍八、小结Token 追踪器的代码量很少,但它把一个黑盒变成了白盒——Agent 每轮花了多少、花在哪了、哪里可以省,全都看得见。回到 Harness 番外篇的视角:Token 追踪是 Harness 的"仪表盘"。没有它,你只知道"任务完成了",但不知道 Agent 用了几轮、每轮输入多少 Token、哪一轮因为工具返回了大量内容导致消耗飙升。有了它,这些问题都有了数据支撑,优化才有方向。 关注 AGENT 魔方公众号,回复 Agent免费领取「从零开始理解 Agent」全套资料包加速入门和掌握 Agent:
-
欢迎阅读「从零开始写好 Skill」系列 —— 上一个系列我们用 7 篇文章拆解了 Agent 的骨架,这个系列教你给 Agent 写"工作手册"。第一篇:Skill 是什么?为什么你应该关心它第二篇:一个好 Skill 长什么样——SKILL.md 的解剖第三篇:手把手写你的第一个 Skill第四篇:写 Skill 太费劲?让 skill-creator 来帮你第五篇:拆开写,串起用——Skill 的组合之道(本文)作者:十一▍开场:一个 Skill 解决一个问题,多个 Skill 解决一整件事前四篇我们一直在聊"一个 Skill"——怎么理解它、怎么拆解它、怎么从零写一个、怎么用工具加速迭代。但真实的工作场景,很少是一个 Skill 就能搞定的。回到贯穿这个系列的例子:你在群里看到一篇好文章,想保存并总结。这件事需要两步——先抓取,再总结。对应两个 Skill:wespy-fetcher 管抓取,article-summarizer 管总结。你不会把"抓取"和"总结"写进同一个 Skill 里。那样 Skill 就会变得臃肿——想只抓取不总结?不行,得连带跑一遍总结。想总结一篇本地已有的文章?不行,得先走一遍抓取流程。两个功能绑死在一起,哪个都不灵活。正确的做法是各管各的,各做各的,需要组合的时候再串起来。但问题来了:分开之后,它们怎么配合?▍一、最简单的配合:接力wespy-fetcher + article-summarizer 就是最典型的接力。用户说:"帮我保存并总结这篇公众号文章。"Agent 判断这件事需要两个 Skill,于是:第一棒:调用 wespy-fetcher,输入公众号链接,抓取文章内容,输出一个 Markdown 文件,保存到本地。第二棒:调用 article-summarizer,读取刚才保存的 Markdown 文件,按照结构化格式输出要点总结。第一个 Skill 的输出,就是第二个 Skill 的输入。这就是接力——上一个 Skill 交出"接力棒"(Markdown 文件),下一个 Skill 接住继续跑。这个过程中,用户只说了一句话。Agent 自己判断该用哪些 Skill、按什么顺序调用。两个 Skill 的作者不需要互相认识,甚至不需要知道对方的 Skill 存在——只要各自的输入输出格式兼容,就能组合。但"格式兼容"这件事不是自动发生的。接力能成立,有一个前提:两个 Skill 之间有一个约定好的"交接格式"。wespy-fetcher 输出的是 Markdown 文件。article-summarizer 的操作流程第一步是"通读全文"——它默认输入就是一篇文本内容。Markdown 对接 Markdown,天然兼容。但如果 wespy-fetcher 输出的是一个特殊的 JSON 结构,而 article-summarizer 只认纯 Markdown 文本呢?接力棒的形状不匹配,交接就断了。这引出第一个设计意识:写 Skill 的时候,要想一步——你的输出能不能被别的 Skill 接住?不需要为某个特定的下游 Skill 做适配,只需要遵循通用的格式约定。输出文本内容就用 Markdown,输出结构化数据就用 JSON,输出文件就存到常规路径。这样任何人写的 Skill 都能和你的组合使用。这其实和 Unix 的管道哲学是一回事:每个程序做一件事,用文本流串起来。Skill 的世界也一样——每个 Skill 做一件事,用通用格式串起来。▍二、进阶配合:分工接力是串行的——A 做完,B 接着做。但有些场景需要并行——多个 Skill 同时处理同一个输入的不同方面。设想一个场景:你写完了一篇技术博客,想在发布前做一次全面检查。你可能需要三个维度的审查:内容准确性:技术细节有没有错误?引用的数据有没有过时?可读性:结构是否清晰?段落过渡是否自然?有没有行话需要解释?发布规范:标题格式、标签、封面图、SEO 描述是否齐全?如果写成一个 Skill,它要同时关注这三个维度,SKILL.md 会非常臃肿,而且很难维护——改一条发布规范就要去翻整个 Skill。更好的做法是三个 Skill 各管一个维度:一个审查内容准确性,一个审查可读性,一个检查发布规范。三个 Skill 看的是同一篇文章,但各自只关注自己的维度。Agent 可以依次调用三个 Skill,也可以并行调用(部分 Agent 支持通过子代理并发执行),最后把三份审查结果汇总成一份完整的检查报告。分工模式的关键是:每个 Skill 的职责边界要清晰,不能互相越界。如果"内容审查 Skill"里也检查了标题格式,"发布规范 Skill"里也评价了内容深度,Agent 就会在汇总时遇到重复甚至矛盾的结论——一个说"标题太长",另一个说"标题信息量够"。这就是为什么第二篇反复强调"概述要划边界"——不只是为了单个 Skill 的准确性,更是为了多个 Skill 协作时不打架。每个 Skill 在概述里写清楚"我管什么、不管什么",就是在和其他 Skill 约定协作边界。▍三、更高级的配合:编排接力和分工都有一个共同点:由 Agent 自己判断该调哪些 Skill、按什么顺序调。大多数简单场景这样就够了。但有些复杂的工作流,步骤固定、顺序不能乱、中间还有条件判断——你不想每次都靠 Agent 临场发挥,希望有一个 Skill 专门负责"指挥"其他 Skill。这就是编排模式:一个 Skill 不干具体的活,只负责按顺序调用其他 Skill,串起整个流程。wlzh/skills仓库(https://github.com/wlzh/skills)里有一个真实案例:youtube-to-xiaoyuzhou。它的任务是把 YouTube 视频转成小宇宙播客,整个流程涉及好几步:调用 video-downloader 下载 YouTube 视频/音频调用 audiocut-keyword 过滤掉音频中的广告关键词调用 voice-changer 对音频进行变声处理调用 image-generator 生成封面图按照小宇宙的发布要求打包上传youtube-to-xiaoyuzhou 自己不会下载视频,不会过滤关键词,不会变声——这些活都是其他 Skill 干的。它只负责一件事:按正确的顺序把这些 Skill 串起来,在它们之间传递参数和文件。它的 SKILL.md 里会直接写明:第一步调用 video-downloader,传入 YouTube 链接拿到音频文件后调用 audiocut-keyword,传入关键词配置过滤完成后调用 voice-changer,传入声音预设……这和前面的"接力"有什么区别?接力模式下,Agent 自己判断"下一步该调什么"。编排模式下,流程写死在 Skill 里,Agent 不需要判断,按剧本执行就行。什么时候该用编排?当一个工作流满足这三个条件时:步骤固定:每次都是同样的几步,顺序不变频繁重复:你发现自己总是手动触发同样的 Skill 组合中间有依赖:上一步的输出是下一步的输入,而且参数传递有讲究如果你只是偶尔"先抓取再总结",让 Agent 自己判断就够了。但如果你每天都在做"下载 → 过滤 → 变声 → 发布"这个固定流程,就值得写一个编排 Skill 把它固化下来。编排 Skill 还有一个好处:它是一个可以分享的工作流。你把 youtube-to-xiaoyuzhou 分享给同事,他不需要知道背后有哪些 Skill、怎么串,一条命令就能跑通整个流程。▍四、反面案例:什么时候不该拆讲完了怎么组合,还要讲一个容易犯的错误:过度拆分。不是所有事情都要拆成多个 Skill。如果你把一个简单任务拆成了三个 Skill,每个 Skill 只做一小步,会出现什么情况?Agent 需要先判断"这件事要调哪几个 Skill",然后判断"按什么顺序调",中间还可能遇到"这步的输出格式和下步的输入不匹配"的问题。整体效果可能还不如一个 Skill 直接搞定。拆分的依据是"复用",不是"好看"。wespy-fetcher 和 article-summarizer 值得拆成两个 Skill,是因为它们各自有独立的使用场景:有时候你只想抓取文章存下来,不需要总结有时候文章已经在本地了(比如你自己写的草稿),只需要总结wespy-fetcher 可以和其他处理类 Skill 组合(比如翻译、朗读)article-summarizer 可以接住任何来源的文本(不只是公众号)每个 Skill 都有独立存在的价值,拆开之后组合可能性反而更大了。但如果两个步骤永远绑定在一起——从来不会单独使用其中一个,那就不要拆。强行拆成两个 Skill 只是增加了复杂度,没有带来任何好处。一个简单的判断方法:问自己——"这个步骤单独拿出来,有没有人会单独用?"如果答案是"有",拆;如果答案是"不可能",留在一起。▍五、让 Skill 更容易被组合:四条设计原则基于前面的分析,总结四条让 Skill 之间更容易配合的设计原则。这些原则不是额外的负担——如果你前几篇的内容都读进去了,会发现它们都是自然推导出来的。原则一:单一职责一个 Skill 只做一件事,做好一件事。wespy-fetcher 只管抓取,不管总结。article-summarizer 只管总结,不管抓取。判断标准很简单:用一句话描述这个 Skill 的功能,如果这句话里需要用"并且"连接两个动作,说明你可能该拆了。"抓取公众号文章并转为 Markdown"——这里的"并且"没问题,因为"抓取"和"转格式"是同一件事的两步,不会单独使用。"抓取公众号文章并总结要点"——这里的"并且"就有问题了,因为"抓取"和"总结"是两件独立的事。原则二:输出格式通用化用 Markdown、JSON、纯文本这类通用格式作为 Skill 的输出。不要发明只有自己能理解的私有格式。通用格式意味着任何下游 Skill 都能接住你的输出,不需要专门适配。你写 Skill 的时候不需要预测它将来会和谁组合——只要输出格式是通用的,组合就是自然而然的事。原则三:边界声明显式化在概述里写清楚"我做什么"和"我不做什么"。article-summarizer 里有一句:"不负责文章抓取(抓取请使用 wespy-fetcher),只处理已有内容的总结。"这句话有三重作用:告诉 Agent 不要拿这个 Skill 去做抓取告诉 Agent 如果需要抓取,去找 wespy-fetcher告诉其他 Skill 的作者,article-summarizer 的对接点在哪里一句话,三重效果。原则四:输入假设最小化你的 Skill 对输入的假设越少,能接住的上游 Skill 就越多。article-summarizer 只要求"一篇 Markdown 格式的文章"。它不关心这篇文章是从哪来的——wespy-fetcher 抓的、x-fetcher 从 Twitter 抓的、video-downloader 转写的字幕、还是用户自己粘贴的,统统都能处理。如果它的要求是"必须是 wespy-fetcher 输出的、包含特定 YAML 头部的、存放在 ~/Documents/articles/ 目录下的 Markdown 文件",那能和它组合的 Skill 就只剩 wespy-fetcher 一个了。假设越少,兼容性越强,组合可能性越大。▍六、从两个 Skill 到 Skill 体系当你积累了十几个 Skill 之后,它们会自然形成层次。拿 wlzh/skills ( https://github.com/wlzh/skills ) 这个仓库举例,里面的 Skill 天然分成了几层:采集层:负责从各种来源获取内容wespy-fetcher:公众号文章x-fetcher:Twitter 推文和长文章video-downloader:YouTube 视频处理层:负责对内容进行加工article-summarizer:文章总结(我们第三篇写的)voice-changer:音频变声text-to-speech:文字转语音输出层:负责把处理结果发布出去youtube-publisher:发布到 YouTubequark-mswnlz-publisher:发布到夸克网盘和资源站采集层的任何一个 Skill 的输出,都能对接处理层的 Skill。处理层的输出,又能对接输出层。层与层之间通过通用格式串联——文本内容用 Markdown,音视频用标准的文件格式。这意味着什么?每加一个新 Skill,就多了一组新的组合可能。加了一个 x-fetcher(抓 Twitter),它就能自动和 article-summarizer(总结)、text-to-speech(朗读)组合。加了一个 youtube-publisher(发布到 YouTube),所有处理层的音视频输出都能对接上去。这不是你预先设计好的,是单一职责 + 通用格式自然涌现的效果。不过别急着追求搭建体系。对大多数人来说,从两三个 Skill 的简单接力开始就够了。当你发现"我总是先抓取再总结再翻译"这种固定组合反复出现时,再考虑要不要把它固化成一个工作流——甚至写一个专门的"编排 Skill"来统一调度。你的 Skill 库就像一个工具箱:先有几把趁手的工具,用着用着自然知道还缺什么、怎么组合最顺手。不要一开始就试图设计一套完美的工具体系——那样你会花大量时间在设计上,而不是在解决实际问题上。▍七、全篇总结单个 Skill 的设计: ✅ 单一职责——只做一件事 ✅ 通用格式——输出别人能接住 ✅ 显式边界——写清楚我管什么不管什么 ✅ 最少假设——对输入要求越少越好Skill 之间的三种配合方式: 接力:A 的输出 → B 的输入(串行) 分工:同一输入 → A 审查维度1 + B 审查维度2 + C 审查维度3 → 汇总(并行) 编排:一个 Skill 当指挥,按固定流程调用其他 Skill(流水线)什么时候拆,什么时候不拆: 拆的标准 = 这个步骤能不能被单独复用 不拆的标准 = 两个步骤永远绑在一起▍下一篇预告这篇我们聊了 Skill 之间怎么配合——接力、分工、以及什么时候不该拆。下一篇是收官。我们把 Skill 放回 Agent 的整体架构中,看它和 Rules、Memory、MCP 的关系,回答一个更大的问题:在 Agent 时代,Skill 处于什么位置?「从零开始写好 Skill」系列是「从零开始理解 Agent」系列的姊妹篇。如果你还没有读过 Agent 系列,建议先从第一篇:Agent 的底层原理开始。 关注 AGENT 魔方公众号,回复 Agent免费领取「从零开始理解 Agent」全套资料包加速入门和掌握 Agent:
-
从零开始理解 Agent」系列文章从第一篇起,我们建立了一个核心心智模型:用户输入 → LLM 思考 → 调用工具 → 观察结果 → 继续思考 → ... → 返回答案。这个循环是 Agent 的大脑。但现实中,有些操作根本不需要经过大脑。作者:十一一、一个尴尬的场景假设你对 Agent 说:/help会发生什么?Agent 把 /help 当作普通用户输入,塞进 messages,发给 LLM。LLM 认认真真地"思考"了一下,生成了一段帮助文本返回给你。能用吗?能用。但问题是:浪费了一次 API 调用。帮助信息是固定的,根本不需要 LLM 生成。浪费了 token。这条消息还会留在 messages 里,占用后续对话的上下文窗口。结果不稳定。LLM 每次生成的帮助文本可能不一样,格式也不统一。再试一个:/clear你只是想清空对话历史,重新开始。但 LLM 不知道你在说什么——它没有"清空 messages 列表"这个工具,只能回复一句"好的,我们重新开始吧",然后 messages 列表一条都没少。这两个例子暴露了一个事实:Agent 的主循环是为"需要思考的任务"设计的,而有些操作天然不需要思考。 让 LLM 处理这些操作,就像让一个高级工程师帮你开关灯——能做,但没必要。二、Command:主循环的旁路解决方案很简单:在用户输入进入 run_agent 之前,先过一道"分流器"。如果输入匹配某个已知命令,直接在本地执行;否则再走正常的 LLM 循环。用户输入 │ ▼以 / 开头?──是──▶ Command Router ──▶ 直接执行,返回结果 │ 否 │ ▼run_agent()(第一篇的核心循环)用第一篇的 run_agent 来说,改动只有一个地方——在调用 run_agent 之前加一层判断:def main(): messages = [{"role": "system", "content": SYSTEM_PROMPT}] while True: user_input = input("\nYou: ").strip() ifnot user_input: continue # ---- 新增:Command 分流 ---- if user_input.startswith("/"): result = handle_command(user_input, messages) if result isnotNone: print(result) continue # ---- 分流结束 ---- messages.append({"role": "user", "content": user_input}) response = run_agent(messages) messages.append({"role": "assistant", "content": response}) print(f"\nAgent: {response}")关键在 continue:命中 command 后,不往 messages 里加任何东西,直接回到等待用户输入。对 LLM 来说,这次交互根本没有发生过。三、实现 Command Router最朴素的 command router 就是一个字典映射:# ---- Command 定义 ----def cmd_help(args, messages): return"""可用命令: /help - 显示本帮助 /clear - 清空对话历史 /model - 切换模型(如 /model gpt-4o) /compact - 压缩对话历史(保留要点) /status - 显示当前状态"""def cmd_clear(args, messages): messages.clear() messages.append({"role": "system", "content": SYSTEM_PROMPT}) return"对话已清空。"def cmd_model(args, messages): ifnot args: return f"当前模型:{os.environ.get('OPENAI_MODEL', 'gpt-4o-mini')}" os.environ["OPENAI_MODEL"] = args[0] return f"模型已切换为:{args[0]}"def cmd_status(args, messages): msg_count = len(messages) model = os.environ.get("OPENAI_MODEL", "gpt-4o-mini") return f"消息数:{msg_count} | 模型:{model}"# ---- Command Router ----COMMANDS = { "/help": cmd_help, "/clear": cmd_clear, "/model": cmd_model, "/status": cmd_status,}def handle_command(user_input, messages): parts = user_input.split() cmd = parts[0].lower() args = parts[1:] if cmd in COMMANDS: return COMMANDS[cmd](args, messages "cmd") return None # 不认识的 / 开头输入,交给 run_agent 处理一共 40 行左右。没有什么新概念,就是最基本的前缀匹配 + 字典分发。注意 handle_command 返回 None 的情况:如果用户输入了 /something_unknown,我们不拦截,而是让 LLM 去处理——也许用户就是想和 Agent 讨论某个以 / 开头的路径。四、/compact——最有意思的 Command前面四个 command 都是纯本地操作,不需要 LLM 参与。但 /compact 不一样:def cmd_compact(args, messages): if len(messages) <= 4: return "对话太短,无需压缩。" # 取出需要压缩的旧消息 system_msg = messages[0] old_messages = messages[1:-2] # 保留最近 2 条 recent = messages[-2:] # 让 LLM 做摘要——注意,这里调用了 LLM summary = llm.chat.completions.create( model=os.environ.get("OPENAI_MODEL", "gpt-4o-mini"), messages=[ {"role": "system", "content": "请用中文简洁总结以下对话的要点,保留关键事实和决策。"}, {"role": "user", "content": str(old_messages)} ] ).choices[0].message.content # 重建 messages messages.clear() messages.append(system_msg) messages.append({"role": "assistant", "content": f"[对话摘要] {summary}"}) messages.extend(recent) return f"已压缩 {len(old_messages)} 条消息为摘要。当前消息数:{len(messages)}"/compact 的特殊之处在于:它是由 command 触发的,但内部还是要调 LLM。 用户显式说"我要压缩",但压缩这件事本身需要 LLM 来做摘要。这打破了"command = 不过 LLM"的简单认知。更准确的理解是:Command 的本质不是"不用 LLM",而是"不走主循环"。Command 的本质不是"不用 LLM",而是"不走主循环"。主循环里,LLM 是决策者——它决定下一步做什么、用什么工具、什么时候停。而 /compact 里,LLM 只是一个被调用的工具——用户已经决定了要做什么(压缩),LLM 只负责执行摘要这个具体动作。如果你读过第六篇(上下文压缩),会发现 /compact 做的事情和第六篇的自动压缩本质相同。区别在于谁来触发:第六篇是 Harness 自动触发(token 数超过阈值),这里是用户手动触发。两条路到同一个终点。五、Command vs Tool vs LLM 自主决策现在我们有三种"让 Agent 做事"的方式。什么时候该用哪种?一个判断原则:如果一个操作的输入和输出都是确定的,不需要 LLM 的"理解"和"判断",就做成 command。 比如"清空对话"——不需要理解,不需要判断,执行就完了。但边界不总是清晰的。拿 /compact 来说,它的触发是确定的(用户说了 /compact),但执行需要 LLM 参与。再比如"上下文快满了要不要压缩"——这件事也可以让 LLM 自己判断,但实践中发现用户显式控制(command)或 Harness 自动判断(第六篇的阈值机制)比让 LLM 自己决定更可靠。六、Command 在真实产品中的样子打开 Claude Code 或 OpenCode 这样的 CLI Agent,你会发现它们的 command 系统远不止 /help 和 /clear:可以看到,command 在实际产品中承担了两类职责:第一类:环境控制。/clear、/model、/mode、/cost 这些操作的对象是 Agent 的运行环境,不是用户的任务。LLM 不需要知道这些事。第二类:快捷入口。/init、/review 本质上是"预设好的 prompt 模板 + 特定 tool 组合"。用户当然可以用自然语言说"帮我审查代码",LLM 也能理解。但做成 command 的好处是:触发确定、行为一致、不会因为 LLM 理解偏差而跑偏。七、和系列其他文章的关系回顾一下 command 在整个 Agent 架构中的位置:用 Harness 番外的话说:Command Router 就是 Harness 的一部分——它是模型之外的、让 Agent 真正能用的基础设施之一。八、总结Command 不是什么高深的设计,就是一个前缀匹配 + 字典分发。但它背后的思维方式值得记住:Agent 的主循环是留给"需要思考的任务"的。 不需要思考的操作——环境控制、状态查询、手动触发——应该绕过主循环,直接执行。这是 Harness 工程的一部分:如何在 LLM 的智能和工程的确定性之间划出合理的边界。Command 选择了确定性的一边——用户说 /clear 就一定清空,不存在 LLM "理解错了"的可能。一句话:不是所有操作都要过大脑。 关注 AGENT 魔方公众号,回复 Agent免费领取「从零开始理解 Agent」全套资料包加速入门和掌握 Agent:
-
欢迎阅读「从零开始写好 Skill」系列 —— 上一个系列我们用 7 篇文章拆解了 Agent 的骨架,这个系列教你给 Agent 写"工作手册"。第一篇:Skill 是什么?为什么你应该关心它第二篇:一个好 Skill 长什么样——SKILL.md 的解剖第三篇:手把手写你的第一个 Skill第四篇:写 Skill 太费劲?让 skill-creator 来帮你(本文)作者:十一 「从零开始写好 Skill」系列 —— 上一个系列我们用 7 篇文章拆解了 Agent 的骨架,这个系列教你给 Agent 写"工作手册"。第一篇:Skill 是什么?为什么你应该关心它第二篇:一个好 Skill 长什么样——SKILL.md 的解剖第三篇:手把手写你的第一个 Skill第四篇:写 Skill 太费劲?让 skill-creator 来帮你(本文)▍开场:三轮迭代的代价还记得第三篇吗?我们从零写了一个 article-summarizer,经历了三轮迭代:第一版 description 触发不了、输出格式不稳定;第二版对照检查清单修了一遍,好多了但实际跑起来又发现观点数量太死板、字段之间撞车;第三版基于踩坑再优化,终于好用了。三轮迭代,每一轮都要自己思考问题在哪、怎么改、改完再跑一遍验证。这还只是一个功能很简单的总结 Skill。如果你要写一个代码审查 Skill、一个部署流水线 Skill,复杂度翻几倍,迭代次数也会翻几倍。有没有办法让这个过程更高效?有。Anthropic 官方出了一个"元技能"—— skill-creator。它本身就是一个 Skill,但它的职责是帮你创建和优化其他 Skill。用 AI 来写 AI 的工作手册。▍一、skill-creator 是什么一句话:它是一个帮你创建、测试和迭代 Skill 的 Skill。它覆盖了 Skill 开发的完整生命周期:理解你的意图——你想让 Skill 做什么起草 SKILL.md——包括 description、操作流程、注意事项生成测试用例——验证 Skill 效果运行对比评估——有 Skill vs 没 Skill,效果差多少基于评估结果迭代优化优化 description 的触发精准度本质上,它把第三篇里我们手工做的三轮迭代,变成了一个系统化的、有数据支撑的流程。你不需要自己猜"description 写得够不够好",它会用 20 个测试查询帮你验证;你不需要自己判断"总结质量提升了没有",它会并排对比有 Skill 和没 Skill 的输出,让你一目了然。▍二、先看它的文件夹结构在讲怎么用之前,先看看 skill-creator 自己长什么样。它是"Skill 是文件夹不是文件"这个概念的最佳实例:skill-creator/├── SKILL.md ← 主文件:完整的创建流程指南├── agents/ ← 子代理定义│ ├── grader.md ← 评分员:评估测试结果│ └── analyzer.md ← 分析员:找出数据背后的模式├── eval-viewer/ ← 评估结果可视化工具│ └── generate_review.py├── references/ ← 参考资料│ └── schemas.md ← 评估数据的 schema 定义├── scripts/ ← 辅助脚本│ ├── init_skill.py ← 初始化新 Skill 的目录结构│ ├── package_skill.py ← 打包 Skill 用于分发│ └── aggregate_benchmark.py ← 聚合评估数据└── assets/ ← 模板等资源一个 SKILL.md 主文件,搭配子代理定义、脚本、参考资料、可视化工具。Agent 读 SKILL.md 了解整体流程,执行时按需调用 scripts/ 里的脚本,评估时启动 agents/ 里定义的子代理,最后用 eval-viewer/ 生成可视化报告。这就是第二篇讲的"渐进式揭示"的教科书案例——SKILL.md 不塞所有内容,而是指向各个子文件,Agent 在需要的时候才去读。▍三、完整流程:从意图到成品安装好 skill-creator 之后,告诉它你想创建什么 Skill,它会引导你走完以下六个阶段。阶段一:意图捕获skill-creator 不会让你一上来就写 SKILL.md。它先用"采访模式"收集需求:"这个 Skill 要解决什么问题?""用户会怎么描述这个需求?给几个具体的说法。""给几个具体的使用场景?""有没有什么边界条件——哪些事情这个 Skill 不该做?"它会根据你的回答追问细节,但不会一次问太多——它的 SKILL.md 里明确写了"避免在一条消息里问太多问题"。这里有一个细节值得注意:skill-creator 会根据你的技术水平调整表达方式。它的 SKILL.md 里有一段很有意思的话——大意是"现在连水管工都开始打开终端了,另一方面大多数用户又是程序员,所以要看上下文线索来决定怎么说话"。比如 "JSON" 和 "assertion" 这种词,它会先确认你是否了解再使用。这个设计本身就是一个好 Skill 该有的样子:不假设用户的背景,根据上下文自适应。阶段二:起草收集完需求后,skill-creator 调用 scripts/init_skill.py 自动生成 Skill 的目录结构:# skill-creator 自动执行python scripts/init_skill.py my-new-skill生成一个标准的 Skill 文件夹,包含 SKILL.md 模板和 scripts/、references/、assets/ 子目录。然后它会基于你在第一阶段提供的信息,填充 SKILL.md 的内容。它生成的 SKILL.md 会自动遵循我们前几篇讲的原则:description 写成触发条件,而且覆盖面写得比较宽。比如不只写"当用户要求创建仪表盘时触发",还会加上"即使用户没有明确说'仪表盘',只要提到了数据可视化、内部指标,都应该触发"。这和第二篇讲的"覆盖用户多种说法"一脉相承,宁可多触发也不要漏触发。内容区分"参考知识"和"任务流程"——该放进 references/ 的不会塞在 SKILL.md 里。可复用的脚本放进 scripts/——确定性的工作交给代码,不靠 AI 每次重写。它还会特别提醒你:写 Skill 是给另一个 Agent 实例看的,重点放在对 Agent 有用但不是显而易见的信息上。 不需要教 Agent "什么是 API",但需要告诉它"调用这个 API 时必须传 X-Auth-Token 头,否则返回 403"。阶段三:测试这是 skill-creator 最有价值的部分。它会为你的 Skill 生成一组测试用例,然后对每个用例同时启动两个子代理:一个带着你的 Skill 执行任务一个不带任何 Skill 执行同样的任务两个子代理并行跑(不是先跑完一个再跑另一个),输出放在一起对比。这直接回答了一个关键问题:你的 Skill 到底有没有用? 不是你主观感觉"好像好了一点",而是有/无 Skill 的输出并排放在面前,差距一目了然。还记得第一篇那个公众号文章的对比吗?没有 Skill 的 Agent 说"我做不到",有 Skill 的 Agent 说"做好了"。skill-creator 把这种对比变成了一个可重复执行的自动化流程——每次修改 Skill 之后都能重新跑一遍,量化改进效果。阶段四:评分和分析测试跑完后,skill-creator 启动两个专门的子代理来处理结果:评分员(grader):对每个测试用例的输出打分。能用脚本程序化验证的断言(比如"输出文件是否存在""格式是否符合 JSON schema"),就写脚本去检查,不靠 AI 的主观判断。只有需要定性评估的部分(比如"总结质量如何")才用 AI 打分。这个设计原则值得记住:确定性的事交给脚本,判断性的事交给 AI。 脚本更快、更稳定、可复用;AI 擅长做需要理解和判断的事,但不该浪费在"检查文件是否存在"这种事情上。分析员(analyzer):在评分数据的基础上找模式。比如:某些断言不管有没有 Skill 都通过——说明这个断言没有区分度,需要改某些测试方差很大——可能是断言本身不稳定时间和 Token 的消耗变化——Skill 是否让 Agent 更高效了最后生成一个交互式的浏览器页面(通过 eval-viewer/generate_review.py),你可以在页面上直观地看到每轮迭代的效果变化、定性输出和定量指标。# skill-creator 自动执行,你也可以手动运行python eval-viewer/generate_review.py \ my-skill-workspace/iteration-1 \ --skill-name "my-skill" \ --benchmark my-skill-workspace/iteration-1/benchmark.json阶段五:迭代看完评估结果,你告诉 skill-creator 哪里不满意、需要怎么改。它会修改 SKILL.md,然后重新跑一轮测试,对比新旧版本的 benchmark 数据。从第二轮迭代开始,评估报告会自动和上一轮对比,你能清楚地看到每次修改带来的变化——通过率是上升还是下降、Token 消耗是增加还是减少、哪些原来失败的测试现在通过了。这就是第三篇里我们手工做的事情——但 skill-creator 把它从"靠感觉"变成了"靠数据"。阶段六(可选):description 优化skill-creator 还有一个专门优化 description 触发精准度的功能。它会生成 20 个测试查询:10 个应该触发这个 Skill 的,10 个不应该触发的。然后检查你的 description 在这 20 个查询上的匹配准确率,最多迭代 5 轮,每轮都调整 description 的措辞,直到准确率满意为止。这个功能直接对应第二篇的关键点——"description 是触发器,不是摘要"。手工优化 description 很靠直觉,而 skill-creator 用 20 个测试查询把它变成了一个可量化的优化过程。▍四、安装和上手一条命令安装:npx skills add https://github.com/anthropics/skills --skill skill-creator安装完成后,在 Agent(如 Claude Code)里就可以直接使用了。如果你想从零创建一个新 Skill,直接告诉它你的需求:"帮我创建一个 Skill,用来在每次提交代码前自动检查是否有遗留的 TODO 注释。"它会引导你走完意图捕获 → 起草 → 测试 → 迭代的完整流程。如果你已经有一个写好的 Skill 想优化,把路径告诉它:"帮我优化 article-summarizer 这个 Skill,我觉得它的总结质量不太稳定。"它会对现有 Skill 做评估,找出问题,给出改进建议。建议的上手路径:先用它优化你已有的 Skill,再用它从零创建新的。 优化一个已有的 Skill 能让你快速熟悉 skill-creator 的评估流程,理解"测试用例""断言""benchmark"这些概念在实际中怎么运作。有了这个基础,再从零创建就顺畅多了。▍五、skill-creator 教给我们的三条设计理念抛开工具本身不谈,skill-creator 的设计里藏着几条值得所有 Skill 作者学习的理念。理念一:description 宁可写宽,不要写窄大多数人写 description 倾向于保守——只在用户明确说出关键词时才触发。skill-creator 的建议是反过来:主动覆盖用户可能的各种说法,甚至用户没有明确提到关键词时也该触发。它给的例子很直观:不要只写"当用户要求创建内部数据仪表盘时使用",而是写"当用户提到仪表盘、数据可视化、内部指标,或者想展示任何类型的公司数据时使用,即使他们没有明确说'仪表盘'这个词"。道理很简单:Skill 多触发一次,Agent 发现不需要,顶多忽略掉,没有损失。但 description 写得太窄导致该触发的时候没触发,用户就得不到帮助。漏触发的代价远大于多触发。理念二:确定性的事交给脚本skill-creator 的测试流程里,能用脚本验证的断言绝不用 AI 打分。"文件是否生成了","JSON 格式是否正确","行数是否在范围内"——这些有明确对错的事情,写一段脚本检查比让 AI 每次"看一眼"更快、更准、更稳定。这个原则适用于所有 Skill 的设计:把确定性的操作封装成脚本放在 scripts/ 目录里,让 AI 专注于需要理解和判断的部分。 AI 的精力应该花在"这段代码的设计是否合理"这种问题上,而不是"这个文件是不是存在"。理念三:Skill 是写给另一个 AI 的skill-creator 在创建 Skill 时有一条很关键的提醒:你写的 Skill 是给另一个 Agent 实例使用的。重点应该放在"对 Agent 有用但不是显而易见的信息"上。什么是"显而易见的"?Agent 已经知道怎么写 Python、怎么调用 REST API、怎么格式化 Markdown——这些不需要你在 Skill 里重复。什么是"不显而易见的"?你们团队的内部 API 要传一个特殊的认证头、某个库的 2.x 版本有一个文档没提到的 bug、部署到生产环境前必须先跑冒烟测试——这些才是 Skill 应该写的内容。Skill 的价值在于填补 AI 的知识盲区,而不是重复 AI 已经知道的东西。▍六、工具和手艺skill-creator 是一个好工具。但工具不能替代理解。如果你跳过前四篇,直接用 skill-creator 来生成 Skill,你会遇到几个问题:它生成的 SKILL.md 你看不懂为什么这样写;评估结果出来了你不知道哪些指标重要;测试失败了你不知道是 Skill 的问题还是测试用例的问题。反过来,如果你理解了 Skill 的本质(第一篇)、知道每个部分怎么写才有效(第二篇)、经历过手工迭代的过程(第三篇),再来用 skill-creator,你会发现——它加速的是你本来就知道该怎么做的事情。意图捕获阶段,你知道该提供什么样的使用场景。起草阶段,你能判断它生成的 description 覆盖面够不够宽。测试阶段,你能设计出有区分度的断言。迭代阶段,你能从 benchmark 数据里读出真正的问题。先学会手艺,再用工具放大效率。这个顺序不能反。下一篇预告到这里,你已经掌握了从理解 Skill、拆解 Skill、手写 Skill 到用工具加速的完整链路。但我们一直在聊"一个 Skill"。真实的工作场景,往往需要多个 Skill 组合——先抓取再总结、先审查再修复、先收集需求再生成方案。下一篇,我们聊怎么拆开写、串起用,以及什么时候该拆、什么时候不该拆。「从零开始写好 Skill」系列是「从零开始理解 Agent」系列的姊妹篇。如果你还没有读过 Agent 系列,建议先从 第一篇:Agent 的底层原理 开始。
-
欢迎阅读【LLM推理】专栏系列文章,在首个系列,我们将带来大模型智能推理方向开源项目Kthena技术解析:★【第一期】《Kthena 核心原语:ModelServing CRD 如何定义分布式推理的“新标准”?》(本文)★【第二期】《Kthena Router:插件架构解析及Benchmark测试》★【第三期】《Kthena AutoScaling:深度解析 Kthena 如何通过“语义感知弹性”重塑 AI 推理成本》★ 项目地址:cid:link_0 随着大模型参数规模呈指数级增长,单一虚拟机或物理机的资源限制已无法满足其需求。为了应对这一挑战,业界引入了许多创新性的部署框架,例如 PD (Prefill-Decode) 分离部署以及大小模型混合部署框架。这些方法彻底改变了推理部署的方式:不再是一个 Pod 处理整个推理任务,现在往往由多个 Pod 协同完成单次推理任务。这种多 Pod 协同已经成为大模型推理部署的关键趋势。在实践中,推理模型可能仍然运行在单个 Pod 内(如传统的单节点场景)、一组相同的 Pod 中(针对更大的模型),或者跨越具有专门角色的多个 Pod(如 PD 分离部署)。这种灵活的部署方式不仅提高了资源利用率,也实现了更高效的大模型推理。ModelServing 是 Kthena[1] 定义的一个API,旨在管理和编排推理模型工作负载的生命周期。得益于其三层架构,它可以非常方便地表示和管理多种部署模式。▍三层架构为了解决 Kubernetes 传统的两层架构(例如 Deployment 和 StatefulSet)在管理多样化推理工作负载部署场景时的局限性,ModelServing 采用了 ModelServing → ServingGroup → Role 的三层架构。架构图如下所示:ModelServing: 核心组件,负责管理推理模型工作负载的生命周期。它提供了一个统一的接口,用于部署和管理推理模型以及查询它们的状态。ServingGroups: ServingGroup 是一个 Role(角色)的集合。每个 Group 都可以完成完整的推理任务,对于 PD 分离部署来说,它同时包含 Prefill 和 Decode 角色。Roles: 一个 ServingGroup 内的每个 Role 由一组 Pod 组成,这些 Pod 是负责执行推理任务的实际工作负载。可以为每个角色分配不同的任务。例如,在 PD 分离场景中,您可以配置一个 prefill role(预填充角色)和一个 decode role(解码角色)。关于 ModelServing 的定义,请参考 modelServing CRD 参考文档[2]。通过将推理工作负载整合为 Group 形式,该架构实现了与 Volcano 的 Gang 调度及网络拓扑感知调度的无缝对接。与此同时,针对滚动更新和弹性扩缩容等核心的云原生工作负载管理需求,我们也进行了专门的扩展处理。▍Gang 调度Gang 调度策略是 Volcano-Scheduler 的核心调度算法之一。它满足了调度过程中“同生共死 (All or nothing)”的调度需求,避免了由于为满足任务需求的部分 Pod 部署而导致的资源浪费。Gang 调度算法会观察已调度的 Pod 数量是否满足最小运行数量。当满足 Job 的最小运行数量时,将对 Job 下的所有 Pod 执行调度动作;否则,所有的Pod都不会执行调度。在 Kthena 中,基于 ModelServing 创建 PodGroups,通过 PodGroups 利用 Volcano 的 Gang 调度能力。subGroupSize 字段指定了每个 Role 中需要进行 Gang 调度的 Pod 数量。实例级 Gang 调度创建过程Kthena将为整个 ServingGroup 实例创建一个单一的 PodGroup。该配置为自动生成,无需手动创建。PodGroup 配置:以下为 modelServing 示例:apiVersion:workload.serving.volcano.sh/v1alpha1kind:ModelServingmetadata:name:samplenamespace:defaultspec:schedulerName:volcanoreplicas:1# servingGroup replicastemplate: restartGracePeriodSeconds:60 gangPolicy: minRoleReplicas: prefill:2 decode:2 roles: -name:prefill replicas:4 # ... additional role configuration -name:decode replicas:4 # ... additional role configurationapiVersion:scheduling.volcano.sh/v1beta1kind:PodGroupmetadata:name:{modelserving-name}-{servinggroup-index}namespace:{modelserving-namespace}labels: modelinfer.volcano.sh/name:{modelserving-name}-{servinggroup-index}annotations: scheduling.k8s.io/group-name:{modelserving-name}spec:minMember:8subGroupPolicy:-labelSelector: matchLabels:`` modelserving.volcano.sh/name:sample modelserving.volcano.sh/role:prefill matchLabelKeys: -modelserving.volcano.sh/role-id minSubGroups:2 name:prefill subGroupSize:2 -labelSelector: matchLabels: modelserving.volcano.sh/name:sample modelserving.volcano.sh/role:decode matchLabelKeys: -modelserving.volcano.sh/role-id minSubGroups:2 name:decode subGroupSize:2## ... another configuration ...pod数量计算:如果未配置 MinRoleReplicas,minMember的值计算如下:minMember = replicas × Σ(role.replicas × (1 + role.workerReplicas))如果配置了 MinRoleReplicas, minMember的值计算变为:minMember = replicas × Σ(minRoleReplicas[roleName] × (1 + role.workerReplicas))其中: replicas: ServingGroup 实例的数量 role.replicas: 每个 ServingGroup 内角色实例的数量 minRoleReplicas[roleName]: 配置的Role需要的最少Replicas 1 + role.workerReplicas: 每个角色实例的 EntryPod + WorkerPods 数量SubGroupSize计算:如果未配置 MinRoleReplicas,subGroupSize 的值生成逻辑如下:针对 Spec.Template.Roles 中指定的每个 role在podGroup中创建对应的subGroupPolicy每个Role对应的SubGroupSize为 (1 + role.workerReplicas)如果配置了 MinRoleReplicas,subGroupSize 的值生成逻辑变为:针对 MinRoleReplicas map中指定的每个 role在Spec.Template.Roles当中找到对应的role在PodGroup中创建对应的subGroupPolicy每个minRoleReplicas中的role对应的SubGroupSize为 (1 + role.workerReplicas)▍滚动更新滚动更新是云服务实现零停机的一项关键运维策略。在 LLM 推理服务场景中,支持滚动更新对于降低服务不可用风险、保障业务连续性至关重要。 目前,ModelServing 支持在 ServingGroup 级别进行滚动升级,允许用户配置 Partitions 控制滚动过程。Partition:表示 ModelServing 更新时划分的分界序号。在滚动更新期间,序号大于或等于 Partition 的副本将被更新。序号小于 Partition 的副本将不会被更新。以下为一个配置了 rollout 策略的 ModelServing示例:spec: rolloutStrategy: type:ServingGroupRollingUpdate rollingUpdateConfiguration: partition:0接下来,我们将展示具有四个副本的 ModelServing 的滚动更新过程。这里模拟了三种副本状态:✅ 副本已更新❎ 副本未更新⏳ 副本正在进行滚动更新在滚动升级期间,控制器会删除并重建需要更新的副本中序列号最大的副本。直到新副本正常运行后,下一副本才会被更新。现阶段ModelServing已经支持设置maxUnavailable,能够设置在滚动更新的时候有多少新版本的servingGroup可以为不可用状态,控制滚动升级的速度。▍扩缩容在云原生基础设施项目中,弹性扩缩容在优化资源成本、提升系统可用性、增强响应能力以及简化运维管理等方面,发挥着至关重要的作用。 在 ModelServing 中,我们支持 ServingGroup 和 Role 的两级扩缩容。 ServingGroup 级别的扩缩容与滚动更新的处理过程类似,更新均在副本集中以逆序进行。 Role 级别的扩缩容可以直接对每个角色的进行副本数细粒度的调整。在 PD 分离部署场景中,可以弹性调整 prefill 或 decode 副本,根据它们各自的工作负载优化 P/D 比例。例如:长提示词,短输出场景:弹性增加 prefill 副本,以处理计算密集的提示词处理,同时保持较少的 decode 副本。短提示词,长输出场景:弹性增加 decode 副本,以处理序列 token 的生成,同时保持较少的 prefill 副本。这种灵活的扩缩容能力确保了能够基于实际工作负载模式进行最佳的资源分配。 通过修改 role.Replicas 触发角色粒度的弹性扩缩容,整个 ServingGroup 的状态将变为 scaling,随后执行 Pod 的创建或删除流程。 当 Pod 副本数满足预期后,ServingGroup 的状态将基于所有 Pod 的状态更新。并且由于 Role 中的 Pod 带有顺序和标签,所有扩缩容操作都是从最后一个 Pod 开始处理的。Role 扩缩容流程▍重启策略在 ModelServing 中,强调了将 Pod 分组的概念。因此,当组内的一个 Pod 发生错误时,通常会一起重启整个组。然而,在生产环境中,重启一整组 Pod 可能会消耗大量的资源和时间。为解决这个问题,ModelServing 也提供了支持单个 Role 重启的策略。ServingGroupRecreate: 当组内 Pod 发生错误时,将重启整个servingGroup。 RoleRecreate: 当组内 Pod 发生错误时,servingGroup的状态将更新为 progressing 并且仅重启受影响的 Role。如果 serviceGroup 停留在 progressing 状态超过一定时间,则将删除并重新创建整个 ServingGroup。▍展 望作为 Kthena 的核心组件,ModelServing 如今已能游刃有余地应对大模型工作负载的管理与调度,彰显了 Kthena 架构的优越性与前瞻性。尽管目前在 PD 分离场景的精细化操作(如实例平滑升级)上尚处于完善阶段,但在未来的发展蓝图中,我们势必会为 PD 分离等复杂部署模式注入更多专属的强大能力。如果您对此感兴趣,我们欢迎您加入 Kthena 社区,共同建设我们的开源生态系统。相关链接:[1] 加入 Kthena 社区: cid:link_1[2] modelServing CRD 参考文档: https://kthena.volcano.sh/docs/next/reference/crd/workload.serving.volcano.sh#modelserving欢迎Star★,Fork,来 Kthena 社区一起玩转LLM推理! 扫码进入Kthena技术交流群社区小助手k8s2222
-
在刚刚闭幕的 KubeCon + CloudNativeCon Europe 2026 上,全球开源精英与产业力量齐聚阿姆斯特丹,共同见证了云原生领域的又一次浪潮。本届大会以“Keep Cloud Native Moving”为主题,传递出一个清晰的信号:云原生已远超资源编排的范畴,正加速进化为AI——尤其是LLM与Agentic AI——的核心运行底座。 开放创新,共建面向 Agentic AI 的智能原生基础设施 作为 CNCF 的持续贡献者与全球云原生产业的引领者,华为云本次参会以 “Powering the Agentic Future” 为核心主旨,全方位展示了面向 Agentic AI 的“智能原生”基础设施开源创新与产品方案。通过多场深度技术演讲、沉浸式展区互动以及前沿技术研讨,华为云向全球开发者系统分享了在 AI 全生命周期管理、大规模异构算力调度、分布式推理流量治理、高性能服务网格等关键方向上的技术突破与实践经验,共同码写云原生在智能时代的新篇章。▍Volcano:从 AI 全生命周期调度到 Agent 韧性底座随着 Agentic AI 的兴起,基础设施面临着从“作业调度”向“复杂智能体编排”的转变。作为业界顶尖的云原生AI调度引擎,Volcano 构建从大规模训练到 Agent 编排的 AI 全生命周期调度底座,并于 2025 年重点推出了 面向 LLM 推理的 Kthena 与 面向 AI Agent 工作负载的高性能编排层 AgentCube 两个备受瞩目的子项目,在技术层面高性能适配 Agentic AI 应用要求。在工作负载层:Volcano-Global 将海量训练作业拆分到多个集群,突破了单集群的限制;Kthena 提供企业级 LLM 服务,并支持 vLLM 等框架;AgentCube 快速实现Agent工作负载调度。在基础设施层,Volcano 通过 DRA 集成、HyperNode 发现、GPU 共享和异构池化,提供现代化的资源抽象,实现高效的任务到加速器映射。▲ Breaking the Monolith: Decomposing and Governing Giant LLM Jobs Across Clusters - Kevin Wang, Huawei通过打通完整 AI 生命周期的统一调度能力,Volcano 提供强大的调度能力和高吞吐量,能够协调各种工作负载,超越批处理作业的限制,实现多调度器协同,高效应对人工智能快速发展过程中的训练、推理和 Agent 工作负载运行在孤立的系统中造成的资源效率低等难题。这不仅是调度算法的优化,更是云原生 AI 基础设施的一次范式重构,为 Agentic Future 提供了稳健、高效的运行动力。▲ 华为云开源技术专家 Zhonghu Xu(上)、Zicong Chen(左)、ZengZeng Yao(右)发表Volcano及Kthena、AgentCube 议题更多Volcano议题演讲精彩内容,欢迎关注后续技术解析。▍Karmada:跨越云端边界,构建面向 AI 的多集群管理架构本届大会,来自华为云的 Karmada 维护者 Hongcai Ren 等组织了专场 Project Meeting ,与开发者与用户深度探讨了多集群扩展性、工作负载分发及社区发展路标,强化了分布式云原生的协作生态。携手Bloomberg、携程等生产用户,Karmada 社区也在分论坛上分享了社区过去一年的关键演进。Karmada 正在从“多集群管理工具”进化为“多集群AI编排底座”:应用优先级调度、联邦资源配额、有状态应用故障迁移等能力持续增强生产级稳定性;AI 作业调度增强与 Volcano Global 的协同,则为超大规模 LLM 任务的跨集群拆分与统一调度提供了坚实基础。同时,Karmada Dashboard 正式发布、Operator 能力持续增强,显著提升了多集群场景的可观测性与运维体验。▲ Karmada 社区专场会议与议题演讲在 ArgoCon 论坛上,华为云开源技术专家联合用户伙伴分享了 Karmada 与 Argo 生态整合的最佳实践。针对混合云多集群场景下渐进式交付缺乏全局协调能力的痛点,他们展示了如何通过 Karmada 与 Argo CD、Argo Rollouts 的整合,构建“定义一次,安全交付到任意集群”的统一平台。通过金丝雀发布示例,现场演示了集成架构与完整工作流,既保留了 Argo 在单集群渐进式交付的成熟能力,又借助 Karmada 的跨集群编排能力,将渐进式交付扩展至全局多集群。该方案已在生产环境中验证,为企业多云应用交付提供了可复用的实践范本。▲ From Canary To Global: Unified Progressive Delivery for Hybrid Cloud With Karmada & Argo - Zhuang Zhang, Huawei & Karmada PartnerKarmada 的成熟度与可靠性已在 Bloomberg、携程等复杂生产环境中得到充分验证,社区用户组成员突破40+。未来,Karmada 社区将持续聚焦 AI 工作负载的跨集群编排能力,与 Volcano、Kueue 等项目深度协同,共同构建面向AI多集群场景的统一管理与控制面▍Kmesh:基于 Rust 的数据面创新,引领Sidecarless服务网格服务网格的性能开销一直是大规模分布式系统痛点,尤其是在对时延极其敏感的 AI 推理场景中。Kmesh 独辟蹊径地采用了内核级、Sidecarless 的架构。通过将服务治理逻辑下沉至 OS 内核(基于 eBPF 技术),Kmesh 实现了近乎零开销的网络转发,极大地降低了服务间通信的时延。▲ Optimize Sidecarless Service Mesh With A Brand-New Rust-Based Proxy - Zengzeng Yao, Huawei Cloud;Kmesh Maintainer华为云开源技术专家分享了在 Kmesh 社区的技术创新成果。针对现有 Sidecarless 服务网格方案中 L7 流量处理依赖 Envoy waypoint 所带来的性能瓶颈与内存管理难题,包括不可预测的内存泄漏和生产环境调试复杂性。Kmesh 引入基于 Rust 重构的 Orion,作为 waypoint 与 Kmesh 深度集成后,Orion 与 Kmesh 的 eBPF L4 处理能力形成合力,构建了覆盖 L4 与 L7 的统一高性能 Sidecarless 服务网格。这一方案既延续了 Kmesh 在 L4 层的极致性能优势,又通过 Rust 的安全内存模型解决了 L7 代理在长期运行中的稳定性隐患,为服务网格在AI时代高吞吐、低延迟场景下的规模化落地提供了全新路径。▍边缘智算引擎,KubeEdge 赋能万物智能正如会上的演讲议题“KubeEdge Everywhere: From Graduation to Global Adoption”,KubeEdge 的行业应用近年来呈爆炸式增长。作为首个从 CNCF 毕业的云原生边缘项目,KubeEdge 自 2024 年晋级后,社区的功能更新、治理更新以及实践案例,充分验证了在边缘 AI 和行业工作负载管理方面的强大性能,其强大的边云协同能力,为千行百业的智慧场景提供了可落地的云原生边缘基础设施技术方案。来自DaoCloud、谐云、Google、华为云的技术专家共建了 KubeEdge 系列议题,与此同时,KubeEdge Project Pavilion 吸引了大量关于边缘 AI 落地场景的讨论。▲ KubeEdge 应用于大会 Keynote 议题演讲中的滑翔机 展区零距离——智算引擎全栈体验 在华为展台,华为云向与会者展示了面向 Agentic AI 时代的智能原生基础设施解决方案。展区围绕 Agentic AI 基础设施与开发者展开深度互动,通过全新一代 CCE 智算集群、华为云 Agent 全栈平台、华为云容器领导力等产品与内容展示,呈现了从云原生基础设施到 AI 时代工作负载最佳运行底座的全栈能力,共同探讨智能原生未来图景。同时,华为云技术团队也分别在 Volcano、Karmada、Kmesh 等多个项目展台驻场,从多集群编排到AI调度,从服务网格到K8s生态,通过现场答疑、案例讲解与代码演示,与开发者进行开源技术创新与应用的面对面深度技术交流。作为云原生与 AI 领域的先驱者,华为云深耕 Kubernetes 等核心技术十余年,在 AI 浪潮中打造面向未来的AI原生基础设施,构建 AI Infra、Agent Infra 等算力底座,支撑 AI 大模型与 Agent 智能体应用需求。凭借多年来的产业实践和技术创新,华为云连续5年蝉联国内容器软件市场份额 TOP1,获选 Gartner 容器管理魔力象限领导者,Omdia 产品战略与执行全球第一,技术实力获全球权威认可。 Powering the Agentic Future 这场阿姆斯特丹的思想碰撞,是云原生基础设施与 Agentic AI 的一场“双向奔赴”;云原生底层逻辑正在升级重塑,成为支撑智能时代运行的“数字神经系统”。在迈向 Agentic Future 的征途中,我们将持续开放创新,让算力更高效、让治理更简洁、让智能更无处不在。期待与全球开源力量并肩,在智能原生的时代浪潮中,跑出加速创新的时代脚步。 更多云原生技术动向关注容器魔方
推荐直播
-
Skill 构建 × 智能创作:基于华为云码道的 AI 内容生产提效方案2026/03/25 周三 19:00-20:00
余伟,华为云软件研发工程师/万邵业(万少),华为云HCDE开发者专家
本次直播带来两大实战:华为云码道 Skill-Creator 手把手搭建专属知识库 Skill;如何用码道提效 OpenClaw 小说文本,打造从大纲到成稿的 AI 原创小说全链路。技术干货 + OPC创作思路,一次讲透!
回顾中 -
码道新技能,AI 新生产力——从自动视频生成到开源项目解析2026/04/08 周三 19:00-21:00
童得力-华为云开发者生态运营总监/何文强-无人机企业AI提效负责人
本次华为云码道 Skill 实战活动,聚焦两大 AI 开发场景:通过实战教学,带你打造 AI 编程自动生成视频 Skill,并实现对 GitHub 热门开源项目的智能知识抽取,手把手掌握 Skill 开发全流程,用 AI 提升研发效率与内容生产力。
回顾中 -
华为云码道:零代码股票智能决策平台全功能实战2026/04/18 周六 10:00-12:00
秦拳德-中软国际教育卓越研究院研究员、华为云金牌讲师、云原生技术专家
利用Tushare接口获取实时行情数据,采用Transformer算法进行时序预测与涨跌分析,并集成DeepSeek API提供智能解读。同时,项目深度结合华为云CodeArts(码道)的代码智能体能力,实现代码一键推送至云端代码仓库,建立起高效、可协作的团队开发新范式。开发者可快速上手,从零打造功能完整的个股筛选、智能分析与风险管控产品。
回顾中
热门标签