• [技术干货] 云原生 Agent 托管的高效范式:Agent Harness Infra 体系化设计
    作者 | Rain Zhang; Qi Zhang; Jian HuangAgent Harness 在云原生托管架构落地的核心意义在于实现弹性、效率与运维的全面优化。借助云原生的虚拟化、容器化与动态编排能力,Agent Harness 能够打破资源闲置的瓶颈,实现按需自动扩缩容,显著降低计算成本。同时,云原生的声明式 API 和服务自愈机制,将复杂的生命周期管理转化为自动化运维,极大减轻了人工负担。此外,依托统一的可观测性与标准化交付流程,Agent Harness 可以无缝集成现有技术生态,加速 AI 代理从原型到生产级应用的转化,从而为企业提供高可靠、低成本且易于治理的智能服务运行底座。▍1. Agent Harness 面向云原生托管的落地挑战当前 Agent Harness 在面向云原生托管架构落地过程中,主要面临冷启动延迟、状态持久化与执行安全三个维度的核心挑战,业界主流方案正通过架构解耦、轻量级虚拟化以及工程化治理来系统性地加以应对。首先,在冷启动延迟和资源浪费方面,传统虚拟机或容器的启动往往需要数秒时间,难以满足 AI 交互场景对实时性的严苛要求。其结果表现为用户等待时间被拉长,使用体验受到明显影响。为了降低延迟,不少系统采用预留“热池”的方式,但这又导致资源利用率在闲时极低,资源浪费严重,而当突发流量来临时,系统的性能表现也不够稳定。其次, 在稳定性与成本控制方面,问题主要体现在上下文窗口的有限性与状态执行的脆弱性上。由于上下文窗口存在上限,任务运行时间较长时容易出现“遗忘”或崩溃现象。一旦沙箱发生故障,正在执行的任务便会直接终止,长任务因此中断且无法恢复,进而引发记忆混乱、运维负担加重以及成本失控等问题。最后,在安全隔离方面,主要风险来自不可信代码的执行以及凭据的泄露。大语言模型生成的代码本身不可信,存在逃逸的潜在威胁。如果凭据与可执行代码同处一个沙箱环境中,提示词注入攻击便可能导致密钥泄露,进而引发系统被破坏、数据泄露、跳板攻击以及权限越级滥用等严重后果。 ▍2. 面向云原生托管 Agent Harness Infrastructure 的设计为了解决上述痛点,华为云托管的 Agent Harness 提出了从架构设计到 Agent Infrastructure 基础设施的解决方案。 对于企业而言, Agent Infrastructure基础设施不再将精力耗费在维护脆弱的单体容器上,而应转向构建 Agent 沙箱容量规划与并行调度、Agent 协调层和执行层架构解耦、具备极简轻量、极速启动、自动恢复能力和安全隔离的 Serverless 沙箱环境。图 1. Agent 猎鹰调度与羽量沙箱Agent 沙箱并行规划与调度通过采用容量预测技术,对 Agent 资源进行精准画像与预热管理。与传统基于时序的算法相比,该模型将拟合精准度提升30%,资源碎片率降低25%,利用率提高10%。在并行调度方面,系统基于资源碎片率、资源余量和预热分配量三个维度的因素,采用分片并行调度机制,使调度吞吐量显著提升至原来的5倍。在生态主导方面,项目在 CNCF 社区内主导了 Volcano 沙箱调度器生态的建设,吸引了超过200家公司参与使用,形成了良好的社区影响力与客户基础。Agent 协调层和执行层架构解耦,实现自动恢复能力采纳轻量级虚拟化技术(microVM),将 Agent Harness 协调层 与 Sandbox 执行层 彻底解耦,支持 Serverless 按需模式,配置合理的闲置超时回收策略。通过 SessionID 保证多轮对话路由到同一实例维持状态,并将会话日志外置持久化。Harness 故障后,新实例可重放日志恢复任务,实现“断点续传”。图 2. Agent  Runtime 运行时架构安全隔离使用 microVM 级 VMM(CloudHypervisor),最小化设备集和每 VM 进程开销(3‑13MiB量级)。在单节点数千并发沙箱规模下,通过 microVM、定制 Guest 环境和动态资源控制,实现 VM 级安全隔离与高密度的兼得。强制隔离 Harness 与 Sandbox,实施最小权限原则与凭据托管。极简轻量华为云针对 Agent 与容器场景进行了极致优化,构建了由“基础操作系统 ContainerOS + 动态生成操作系统 On-the-fly OS”相结合的组合方案,  实现羽量级虚拟化,匹配 Agent 和 Serverless 场景对开销和速度的极致诉求。其中,ContainerOS 仅包含运行容器所必需的基础服务,On-the-fly OS 根据 Agent 运行需求组装和构建所需的 OS 的增量系统 。该方案采用轻量化的内核与根文件系统,可实现秒级启动,且空载状态下的内存占用低于50M字节。作为不可变基础设施,基础操作系统的根文件系统为只读,并以镜像为粒度进行原子化的升级与回滚操作。极速启动通过对 Sandbox 依赖资源及关键流程的预置,系统在计算、网络、存储及启动文件等方面提前准备,将资源准备时间从秒级压缩至毫秒级。在启动优化方面,采用操作系统裁剪与共享内存技术加速虚拟机启动,同时结合快照启动、Fork 机制以及容器组件的预热与重用,使实例创建时间从十秒级缩短至100毫秒。此外,基于预热实例的分层管理能力,系统根据供给性能构建分层预热池,并依据客户使用特征持续优化预热策略,最终将预热命中率提升至冷启动实例占比的80%。图3. Agent Sandbox 启动过程 ▍3. 工作展望:面向 AI Agent 与 Serverless 场景的极致高效、低成本的云原生沙箱体系围绕云原生架构, 后续工作会持续打磨优化 AI Agent 与 Serverless 场景的极致高效、低成本的安全沙箱体系,核心方案围绕以下三个目标展开:图4. 基于预调度 + Snapstart + lazyloading的microVM启动首先,为了应对云原生时代不同场景对安全隔离与弹性效率的多样化需求,以 CNCF (云原生计算基金会)旗下的多沙箱容器运行时项目 Kuasar 为底座,通过采用单 VM 单应用的极简架构,并剔除 Guest Agent 等冗余组件,打造出轻量化的 Appliance Sandbox 模式,目标是使单沙箱的底噪降低20%。图5. 云原生计算基金会CNCF多沙箱容器运行时项目Kuasar其次,为了实现亚秒级甚至百毫秒内的极速启动,方案扩展了 VMM 以支持基于 UFFD 的内存缺页 Hook,实现内存懒加载,并将 Snapstart 作为 Kuasar 的标准启动方式,同时结合虚拟机内存只读页面的复用技术,在降低资源消耗的同时确保单沙箱启动延迟小于100毫秒。最后,为了支撑大规模、高并发的创建需求,即持续10分钟每分钟创建10万个沙箱,方案设计了一个基于块级复用与内容寻址技术的镜像分发底座。该底座在多租户云系统中,将不同租户的镜像数据切块并计算指纹:相同指纹的数据块在多租户间复用,不同指纹的则按租户隔离存储,同时通过全链路块级加密保障安全合规。最终,这一设计在同构工作负载下实现了10倍的存储与带宽缩减,大幅降低了总成本,达成了成本优化与安全合规的双重竞争力。图6. microVM 的快速启动和批量创建 ▍4. 总结面向云原生托管的 Agent Harness 为企业提供了一套完整的 Agent 基础设施解决方案,核心在于将精力从维护单体容器转向构建 Serverless 沙箱环境。该方案通过容量预测与分片并行调度,显著提升了资源利用率和调度吞吐量;利用轻量级虚拟化技术实现协调层与执行层解耦,支持断点续传的自动恢复。在安全方面,采用 microVM 级隔离实现高并发下的安全防护。同时,通过组合式轻量操作系统实现低内存占用与秒级启动,并借助资源预置、快照及预热重用等优化,将实例创建时间缩短至100毫秒,预热命中率达冷启动的80%,从而构建出极简轻量、极速启动且安全隔离的沙箱环境。 关注魔方公众号,获取更多前沿资讯添加社区小助手k8s2222,进入技术交流群
  • [技术干货] Kubernetes 跑 AI Agent,缺的不只是算力——AgentCube 补上了什么
    一次典型的 Code Interpreter 调用,往往从一个很小的动作开始:用户点击“运行代码”。但在一个原生 Kubernetes 环境中,这个动作背后通常意味着一整套秒级链路:调度 Pod、分配网络、拉取镜像、启动容器。对于需要多步推理、频繁调用工具、强依赖交互体验的 AI Agent 应用来说,这样的启动路径并不自然。这也是 AgentCube 试图解决的问题:在 Kubernetes 的算力底座之上,补齐面向 Agent 工作负载的关键机制缺口,提供一层真正理解会话、沙箱和工具调用语义的 Serverless 编排层。AgentCube(cid:link_2) 提供了一组可直接落地的核心机制。本文重点展开这些能力在实现层面的设计取舍:会话如何绑定沙箱、预热池如何消除冷启动、PicoD 如何替代 SSH、JWT 安全链如何保护外部访问,以及系统如何自动回收不再活跃的沙箱资源。  从 Kubernetes 到 Agent Native  随着大语言模型(LLM)技术的成熟,技术架构正从“无状态推理”向“自主智能体(Autonomous Agents)”演进。Kubernetes 凭借成熟的生态和对异构算力的标准化管理,已经成为构建 AI 基础设施的事实标准。但在面对 AI Agent 这种“高并发、短时效、强状态依赖”的新型负载时,原生 Kubernetes 仍存在几类明显的粒度错配与机制缺位。启动延迟与交互体验的矛盾。 Agent 的交互通常要求毫秒级响应,而原生 K8s Pod 的启动流程往往是秒级甚至分钟级。对于需要频繁拉起 Code Interpreter 或临时子 Agent 的场景,这样的冷启动延迟很难被终端用户接受。资源利用率的挑战。 Agent 是典型的 IO 密集型负载。在一次会话中,大量时间都消耗在等待 LLM 返回 Token 或等待外部工具响应。如果在 K8s 上为每个 Agent 独占一个 Pod,CPU 和内存资源会在等待期间被大量闲置。会话状态管理的缺失。 K8s 对无状态工作负载天然友好,但 Agent 高度依赖上下文(Context / Memory)。Pod 的重建意味着内存态上下文的丢失,开发者被迫在应用层额外拼接外部状态存储,系统复杂度和网络开销都会随之上升。安全隔离难题。 高级 Agent(如 Data Analyst)需要运行由 LLM 生成的不可信代码。普通的 runC 容器很难为这种场景提供足够强的隔离边界,企业级 Agent 平台需要一种既能快速启动、又具备强隔离能力的沙箱环境。围绕这些问题,AgentCube 给出了一组明确的工程实现:Kubernetes 面临的缺口AgentCube 的对应机制冷启动延迟高Warm Pool 预热池,提前准备可分配的沙箱实例会话请求需要保持上下文Session ID 到沙箱实例的绑定式路由不可信代码执行风险高MicroVM 沙箱 + PicoD 执行面 + JWT 认证链会话结束后资源容易堆积基于空闲超时和绝对时长的双策略 GCAgentCube 不是简单增加几条 CRD,而是在 Kubernetes 之上补上一层面向 Agent 工作负载的运行时与编排机制。  整体架构  AgentCube 的核心架构可以分为三部分:数据平面(Router)、控制平面(Workload Manager) 和 沙箱执行面(PicoD)。系统通过 Redis/Valkey 维护会话状态和索引,使不同组件可以独立扩展。图 1:AgentCube 整体架构 —— Router 接收请求并路由至沙箱,Workload Manager 管理沙箱生命周期,PicoD 负责沙箱内执行核心组件一览:组件角色关键能力AgentCube Router数据平面入口HTTP 反向代理、会话路由、JWT 签名、并发控制Workload Manager控制平面沙箱创建/删除、预热池管理、双策略 GCPicoD沙箱内守护进程代码执行、文件 I/O、JWT 认证、路径沙箱化Session Store状态存储Redis/Valkey 支持,Sorted Set 索引加速查询一次典型请求的大致链路如下:+----------+ +----------+ +------------------+ | Client | --- 1) HTTP ---> | Router | --- 2) alloc --> | Workload Manager | +----------+ +----+-----+ +--------+---------+ | | 4) sign JWT 3) create/claim + proxy Sandbox Pod | | v v +--------+ +-------------+ | PicoD | <-- running in --- | Sandbox Pod | +---+----+ +-------------+ | 5) verify JWT execute cmd return result | v stdout / stderr / exit_code图中的 Sandbox Pod 底层是由 Kuasar 调用 CloudHypervisor 拉起的 MicroVM,PicoD 作为守护进程运行在 MicroVM 内部,负责接收来自 Router 的请求并在隔离环境中执行。  核心特性深度解读  一、Session-Based MicroVM Routing:让会话真正绑定执行环境AI Agent 工作负载的一个本质特征,是有状态、交互式,并且可能跨越多次调用。一次完整的任务往往包含多步推理、工具调用、环境探查和中间结果写入。如果每个请求都被路由到新的执行环境,上下文就无法自然延续。AgentCube 通过 Session ID -> 沙箱实例 的绑定关系解决这个问题:客户端首次调用时不携带 x-agentcube-session-id,Router 会为其分配新的沙箱实例响应头中返回 x-agentcube-session-id客户端后续请求携带该 Session ID,即可继续命中同一个会话沙箱从客户端视角看,整个过程是透明的。它不需要理解底层沙箱的生命周期,只需要在多轮调用中带上同一个 Session ID。Router 基于 Gin 构建,核心 handler handleInvoke() 的逻辑大致如下:// 从请求头提取 Session ID sessionID := c.GetHeader("x-agentcube-session-id") // 通过 SessionManager 查找或创建沙箱 sandbox, err := s.sessionManager.GetSandboxBySession( c.Request.Context(), sessionID, namespace, name, kind) // 更新会话最后活跃时间(用于 GC 判定) s.storeClient.UpdateSessionLastActivity(ctx, sandbox.SessionID, time.Now()) // 反向代理转发至沙箱 s.forwardToSandbox(c, sandbox, path)Router 同时支持 HTTP/2 (h2c) 透传,并内置可配置的并发请求上限(默认 1000),避免突发流量直接冲垮后端沙箱集群。当前暴露的调用入口包括:POST /v1/namespaces/{namespace}/agent-runtimes/{name}/invocations/*path GET /v1/namespaces/{namespace}/agent-runtimes/{name}/invocations/*path POST /v1/namespaces/{namespace}/code-interpreters/{name}/invocations/*path GET /v1/namespaces/{namespace}/code-interpreters/{name}/invocations/*path二、两种 CRD:两种工作负载抽象AgentCube 在 Kubernetes 之上引入了两类核心 CRD,对应 AI Agent 领域里两种典型的运行模式:一种强调通用能力与灵活性,一种强调安全默认与沙箱约束。AgentRuntime:面向通用 Agent 的运行时抽象AgentRuntime 面向需要完整 Kubernetes 能力的对话式或工具调用型 Agent。它接受完整的 PodSpec 模板,因此可以挂载 Volume、注入凭据、配置 Sidecar 容器。apiVersion: runtime.agentcube.volcano.sh/v1alpha1 kind: AgentRuntime metadata: name: my-agent spec: podTemplate: containers: - name: agent image: my-agent:latest ports: - containerPort: 8080 targetPort: - pathPrefix: "/" port: 8080 protocol: HTTP sessionTimeout: 15m maxSessionDuration: 8hCodeInterpreter:面向安全代码执行的受约束抽象CodeInterpreter 更适合 Notebook、REPL、“运行代码”按钮等多租户安全执行场景。相比 AgentRuntime,它采用更受约束的模板能力,以便将隔离、运行时和资源限制统一收敛到更安全的默认值。apiVersion: runtime.agentcube.volcano.sh/v1alpha1 kind: CodeInterpreter metadata: name: my-interpreter spec: template: image: ghcr.io/volcano-sh/picod:latest runtimeClassName: kata resources: requests: { cpu: "500m", memory: "512Mi" } limits: { cpu: "2", memory: "2Gi" } warmPoolSize: 3 authMode: picod sessionTimeout: 15m maxSessionDuration: 8h两者的核心差异在于:AgentRuntime 更开放,优先满足复杂 Agent 应用的组合能力CodeInterpreter 更收敛,优先满足安全沙箱场景的开箱即用三、Warm Pool:预热池如何消除冷启动从零创建沙箱实例,会不可避免地引入启动延迟。对于交互式 Agent 场景,这种等待通常是用户最直接感知到的体验问题。为此,AgentCube 引入了 Warm Pool 预热池机制。+-----------------------------------------------------------+ | Warm Pool | | | | +-----------+ +-----------+ +-----------+ | | | Sandbox | | Sandbox | | Sandbox | <- idle | | | (idle) | | (idle) | | (idle) | | | +-----+-----+ +-----------+ +-----------+ | | | | | v new session --> SandboxClaim | | +-----------+ | | | Sandbox | <- claimed, ready to use | | | (active) | | | +-----------+ | | | | pool replenishes asynchronously | +-----------------------------------------------------------+Workload Manager 中的 CodeInterpreterReconciler 通过三层 CRD 协作完成这一机制:1. SandboxTemplate:定义沙箱实例模板,并注入 PicoD 所需的认证公钥2. SandboxWarmPool:声明预热副本数,维持一组可被直接认领的空闲实例3. SandboxClaim:新会话到来时,从池中认领一个已就绪实例这种做法的好处,是将“创建实例”和“分配实例”解耦。沙箱启动的慢路径前置到后台完成,用户请求只需走认领路径,因此可以显著缩短首跳延迟。整个过程是幂等的。Reconciler 在调和循环中只对不一致状态做最小变更,这也使 Warm Pool 的管理方式天然贴合 Kubernetes 声明式控制器的工作模型。面向更进一步的启动加速,社区后续计划集成 Kuasar Snapstart,通过内存快照进一步将沙箱就绪时间从秒级压缩至百毫秒量级。四、PicoD:替代 SSH 的轻量级沙箱守护进程传统代码沙箱方案通常依赖 SSH 进行远程执行。但对一个本质上是单请求 RPC 的场景来说,SSH 会引入过重的协议开销,包括密钥管理、多路复用协商和持久会话维护。PicoD(Pico Daemon)就是在这样的背景下被引入的。它运行在沙箱内部,通过一组精简的 HTTP API 代替 SSH,完成命令执行、文件读写和健康检查等操作。API 端点方法功能/api/executePOST执行任意命令,支持超时、工作目录、环境变量/api/filesPOST文件上传或写入(multipart 或 base64 JSON)/api/files/*pathGET文件下载或读取,支持流式传输/healthGET健康检查(免认证)PicoD 运行在 MicroVM 内部,外部请求在触达它之前已经穿越了 VM 级隔离边界。Router 对每个请求签发短期 JWT,PicoD 校验通过后才执行——这道认证门与 MicroVM 隔离分别在两个层次上独立生效。其执行逻辑的核心实现大致如下:ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() cmd := exec.CommandContext(ctx, command[0], command[1:]...) cmd.Dir = workspaceDir cmd.Env = mergedEnv if ctx.Err() == context.DeadlineExceeded { exitCode = 124 }对应的防护措施包括:路径沙箱化:通过 sanitizePath() 将文件访问限制在指定 workspace 根目录下请求体大小限制:默认 32 MB,防止恶意请求耗尽内存无状态处理模型:每个请求独立处理,避免持久连接带来的额外攻击面五、JWT 安全链:外部访问沙箱时,如何建立信任沙箱实例是临时资源,会随时被创建和销毁。如果在集群中直接分发共享密钥,密钥轮换和安全边界都会变得脆弱。为此,AgentCube 在 Router 与 PicoD 之间建立了一条基于 RSA 非对称加密 的认证链。+--------------+ +-------------------------+ | Router | | Kubernetes Secret | | | generate on boot | picod-router-identity | | RSA-2048 | ------------------> | | | key pair | | private.pem (Router) | | | | public.pem (PicoD) | +------+-------+ +------------+------------+ | | | sign with Workload Manager injects | private key PICOD_AUTH_PUBLIC_KEY | (5min TTL) into sandbox env | | v v +--------------+ RS256 JWT Token +--------------+ | Router signs | ------------------> | PicoD verify | | (private key)| | (public key) | +--------------+ +--------------+这条链路解决的是访问认证问题:Router 对外接收请求,对内使用私钥签发短时有效的 JWT;PicoD 持有公钥,负责校验请求是否合法。关键设计点包括:5 分钟短有效期:即使 Token 泄漏,影响范围也能被压缩到很小1 分钟时钟漂移容忍:兼顾分布式环境中的时钟偏差私钥不出 Router:PicoD 仅做校验,不持有签发能力启动时自动生成并注入:降低运维配置复杂度六、双策略 GC:如何回收不再活跃的沙箱在 Agent 场景下,资源创建不是难点,难点在于如何及时、稳定地回收已经闲置的沙箱。如果会话终止后资源不能自动回收,集群最终会被大量“已经没人使用、但仍然占资源”的实例拖垮。为此,AgentCube 在 Workload Manager 中实现了双重垃圾回收策略:策略触发条件默认值空闲超时(Idle TTL)沙在 sessionTimeout 内无任何请求15 分钟绝对最大时长(Max Duration)沙箱创建时间超过 maxSessionDuration8 小时GC 循环每 15 秒执行一次,每轮最多检查 100 个候选沙箱,避免单次 GC 扫描时间过长。底层索引结构基于 Redis Sorted Set:session:{sessionID} -> SandboxInfo JSON session:expiry -> Sorted Set (score = 到期时间戳) session:last_activity -> Sorted Set (score = 最后活跃时间)这种设计让系统可以用 ZRANGEBYSCORE 高效找出已过期或长期不活跃的会话。删除时,GC 会同时清理 Kubernetes 中的 Sandbox / SandboxClaim 资源和 Redis 中的会话记录,避免出现“实例删了但索引还在”或“索引删了但实例还活着”的不一致状态。  生态集成  除了运行时和沙箱机制,AgentCube 还提供了面向上层框架的接入能力,使应用开发者不需要直接处理底层基础设施细节。Python SDKfrom agentcube import CodeInterpreterClient interpreter = CodeInterpreterClient() result = interpreter.run_code("python", "print('Hello, AgentCube!')") print(result) interpreter.write_file(content="data", remote_path="/workspace/data.txt") interpreter.download_file("/workspace/data.txt", "./local_data.txt")LangChain / LangGraph 集成AgentCube 可以作为 LangChain 的 @tool 接入 ReAct Agent 工作流,将代码执行部分自然下沉到受隔离的沙箱环境中,应用层无需直接理解底层实例调度和生命周期管理。Dify 插件项目仓库中的 dify-plugin 目录提供了 Dify 工具接入方式,使 Dify 用户也可以直接消费 AgentCube 的沙箱能力。  快速上手  前置条件Kubernetes 集群 v1.24+Redis 或 Valkey 实例sigs.k8s.io/agent-sandbox v0.1.1 CRD 已安装Helm 安装helm install agentcube manifests/charts/base \ --namespace agentcube-system --create-namespace \ --set redis.addr=<redis-host>:6379 \ --set redis.password="<password>"创建第一个 CodeInterpreterapiVersion: runtime.agentcube.volcano.sh/v1alpha1 kind: CodeInterpreter metadata: name: my-interpreter namespace: default spec: template: image: ghcr.io/volcano-sh/picod:latest resources: requests: { cpu: "500m", memory: "512Mi" } limits: { cpu: "2", memory: "2Gi" } warmPoolSize: 2 sessionTimeout: 15m maxSessionDuration: 8h发起调用curl -X POST \ http://<router-host>/v1/namespaces/default/code-interpreters/my-interpreter/invocations/api/execute \ -H "Content-Type: application/json" \ -d '{"command": ["python3", "-c", "print(1+1)"]}'响应头中的 x-agentcube-session-id 即为会话 ID。后续请求继续携带它,就可以复用同一个沙箱执行环境。  致 谢  从项目启动到今天,AgentCube 已经吸引了多位社区开发者参与共建。感谢所有为 AgentCube 做出贡献的开发者:@YaoZengzeng@acsoto@hzxuzhonghu@Sagar-6203620715@mahil-2040@t2wang@FAUST-BENCHOU@tjucoder@LaynePeng@yashisrani@katara-Jayprakash@LiZhenCheng9527@Tweakzx@warjiang@LeslieKuo@MahaoAlex@VanderChen@kevin-wangzefeng@ifelseend@cairon-ab@RushabhMehta2005@Sanchit2662@qizha@ssfffss@wjf295004046@Aman-Cool@Storm1289@sicaario   相关链接[1] GitHub 仓库: cid:link_2[2] Volcano 官网: https://volcano.sh[3] 设计文档: cid:link_0[4] Python SDK: cid:link_1AgentCube 开源项目欢迎广大开发者参与。无论是提交 Issue、贡献 PR,还是在你的项目中试用 AgentCube,都是对项目的支持。 关注魔方公众号,获取更多前沿资讯添加社区小助手k8s2222,进入技术交流群
  • [技术干货] 告别低效 HPA:深度解析 Kthena Autoscaler 如何重塑大模型服务弹性
    随着大语言模型(LLM)成为现代 AI 应用的核心引擎,支撑其运行的基础设施范式也随之进化。在解决了“智能路由”与“模型编排”等空间维度的请求分发问题后,运维的核心焦点转向了时间维度的资源博弈:如何实时、动态地确定最佳推理实例规模?Kthena Autoscaler 便是针对这一命题的标准答案。作为内置于 kthena-controller-manager 的核心控制器,它深度集成于 Kubernetes 生态,能够基于实时负载特征自动平滑地调整推理服务实例数。其核心价值在于:在严守业务 SLO(服务等级目标)红线的同时,最大化榨取计算资源的利用效率。本文将深入剖析 Kthena Autoscaler 的架构拓扑、通用策略逻辑以及多样化的绑定形态。 ▍1. 为什么 LLM 推理需要专用弹性伸缩?LLM 推理工作负载具有独特的特征,对传统弹性伸缩方案提出挑战:特征对伸缩的影响业务指标驱动相比于 CPU/内存利用率,推理引擎(如 vLLM)暴露的队列长度、KV Cache 利用率等业务指标更能直接反映服务饱和度。突发流量模式用户请求突然激增时需要快速扩容以维持延迟 SLOPrefill/Decode 不对称PD 解耦部署需要对预填和解码角色进行独立且灵活的伸缩。异构硬件与成本不同实例类型(GPU/NPU)提供不同的性能/成本权衡,需要精细化调度。传统的 Kubernetes HPA 或 KEDA 缺乏针对 LLM 工作负载的模型感知能力。Kthena Autoscaler 通过 直连 Pod 采集业务指标、角色级伸缩支持 以及 成本感知优化算法 弥合了这一差距。 ▍2. 架构概览Kthena Autoscaler 遵循控制器模式,作为 kthena-controller-manager 的子控制器运行。它通过直接采集 Pod 业务指标,结合用户定义的策略进行闭环控制。▍3. 通用策略 (AutoscalingPolicy):定义“如何缩容”AutoscalingPolicy 是一个通用的逻辑模板,定义了计算副本需求的核心大脑。3.1 核心指标与容差Autoscaler 允许直接从 Pod 的 /metrics 端点抓取推理专属指标。这意味着它能感知到 vLLM 内部的请求队列状态。常见指标包括:vllm:num_requests_waiting:等待队列长度(最核心指标)。vllm:kv_cache_usage_perc:KV Cache 利用率。通过 targetValue 设置目标值,并利用 tolerancePercent(容差带)防止在目标值附近的微小抖动触发频繁伸缩。3.2 伸缩行为:稳定模式与紧急模式为了应对推理场景的流量特性,Policy 支持双模式策略:稳定模式 (Stable Mode):使用较长的稳定窗口(如 1 分钟)观察持续趋势,避免对瞬时波动过度反应。紧急模式 (Panic Mode):当指标严重偏离目标(如超过 150%)时触发,绕过稳定窗口实现秒级快速扩容。apiVersion: workload.serving.volcano.sh/v1alpha1kind:AutoscalingPolicymetadata: name:vllm-queue-policyspec: metrics: - metricName:vllm:num_requests_waiting targetValue:100 tolerancePercent:50 behavior: scaleUp: stablePolicy: stabilizationWindow:1m period:30s scaleDown: stabilizationWindow:5m period: 1m3.3 成本感知优化算法当伸缩涉及多个实例类型或硬件时,Policy 底层的算法引擎会执行带倍增策略的贪心算法。该算法根据每种实例类型的单位成本 (Cost) 将容量划分为指数级批次(基于 costExpansionRate),并按成本升序排序生成伸缩序列。这确保了:成本效率:优先选择低成本实例。减少冷启动:序列在周期内保持稳定,优先复用已运行的实例。 ▍4. 伸缩绑定 (AutoscalingPolicyBinding):定义“缩容什么”AutoscalingPolicyBinding 是连接通用策略与具体目标的“粘合剂”。通过不同的绑定目标,可以实现完全不同的伸缩形态。4.1 作用于 ServingGroup:实现固定 PD 比例伸缩这是最常见的形态。通过target将 Policy 绑定到 ModelServing 或其中的 ServingGroup。逻辑:Autoscaler 将整组作为一个整体进行扩缩。效果:系统会严格保持定义的 Role 比例(如 prefill:decode = 1:2)同步增减。这适用于 PD 拓扑固定的标准部署场景。# 绑定到 ModelServing (整组同步伸缩)apiVersion:workload.serving.volcano.sh/v1alpha1kind:AutoscalingPolicyBindingmetadata: name:vllm-group-bindingspec: policyRef: name:vllm-queue-policy homogeneousTarget: target: targetRef: kind:ModelServing name:vllm-llama3 minReplicas:1 maxReplicas: 104.2 作用于 Role:实现独立 PD 异构伸缩AutoScaler通过subTargets,能够将 Policy 绑定到 ModelServing 内特定的 Role(如仅绑定 decode 角色)。逻辑:Autoscaler 仅针对该特定角色计算并修改副本数。效果:可以实现 prefill 副本保持稳定,而 decode 副本根据长输出负载独立增加。反过来说,也可以实现decode副本保持稳定,扩缩prefill副本数。这种PD 异构伸缩能极大提高资源利用率。# 包含 Role 定义的 ModelServing 示例apiVersion:workload.serving.volcano.sh/v1alpha1kind:ModelServingmetadata: name:deepseek-servingspec: template: roles: - name:prefill replicas:1 # ... 容器配置 ... - name:decode replicas:2 # ... 容器配置 ...---# 独立绑定到 Role 的示例apiVersion:workload.serving.volcano.sh/v1alpha1kind:AutoscalingPolicyBindingmetadata: name:decode-independent-bindingspec: policyRef: name:llm-scaling-policy homogeneousTarget: target: targetRef: kind:ModelServing name:deepseek-serving subTargets: kind:Role name:decode# 仅针对 decode 角色独立伸缩 minReplicas:2 maxReplicas: 8 ▍5. 最佳实践与故障排查配置建议保守起步:初始配置使用较宽容差带 (15-20%) 和较长稳定窗口。角色差异化目标:在 PD 异构场景下,为 decode 角色设置比 prefill 更敏感的阈值。成本校准:异构伸缩时,根据实际云定价或 TCO 调整 cost 值。可观测性Kthena Autoscaler 在 /metrics 暴露以下指标:kthena_autoscaler_desired_replicas:决策后的目标副本数。kthena_autoscaler_current_replicas:实际观测到的副本数。kthena_autoscaler_scaling_events_total:伸缩动作计数器。 ▍6. 进阶:成本感知优化与异构伸缩示例在实际生产中,我们往往拥有不同规格的 GPU 资源。Kthena Autoscaler 的 heterogeneousTarget 允许在多个目标之间进行成本优先的伸缩分配。# 跨硬件成本优化绑定示例apiVersion:workload.serving.volcano.sh/v1alpha1kind:AutoscalingPolicyBindingmetadata: name:heterogeneous-cost-bindingspec: policyRef: name:vllm-queue-policy heterogeneousTarget: params: - target: targetRef: kind:ModelServing name:deepseek-h100# 性能高,成本高 cost:100 minReplicas:1 maxReplicas:10 - target: targetRef: kind:ModelServing name:deepseek-a100# 成本低,优先扩容 cost:50 minReplicas:1 maxReplicas:20 # 定义成本扩张率,影响算法对成本与容量的权衡 costExpansionRatePercent: 200通过配置不同的 cost 值,Autoscaler 的算法引擎会优先尝试在低成本资源上扩容,而在缩容时则优先保留高效率或特定成本的实例,从而在满足性能需求的同时实现最优 TCO。 ▍总结Kthena Autoscaler 通过将“伸缩逻辑 (Policy)”与“伸缩目标 (Binding)”解耦,提供了极大的灵活性。通过 ServingGroup 绑定可以实现稳定的固定比例扩缩,而通过 Role 绑定则能实现精细的异构扩缩。结合内置控制器的架构和成本感知算法,它为构建高效、低成本的 LLM 推理平台提供了坚实基础。 相关链接[1] Kthena 官方文档: https://kthena.volcano.sh[2] GitHub 仓库: cid:link_0欢迎Star★,Fork,来 Kthena 社区一起玩转LLM推理!关注魔方公众号,获取更多前沿资讯添加社区小助手k8s2222,进入Kthena技术交流群
  • [公告] Karmada 用户组再迎新成员 | GMI Cloud 正式加入!
    Karmada 用户组再迎重要新成员,GMI Cloud[1]正式加入。作为云原生计算基金会(CNCF)旗下的项目,Karmada 致力于为用户提供强大的多集群管理和调度能力,帮助企业在复杂的分布式环境中实现高效的应用部署和管理。GMI Cloud 的加入将进一步加强 Karmada 社区,为项目的持续创新带来新的活力,标志着社区发展和 Karmada 在多样化生产环境中采用的又一个重要里程碑。   关于GMI Cloud  GMI Cloud 是一款面向生产级人工智能推理场景打造的原生 AI 基础设施平台。从无服务器应用程序接口到专属 GPU 集群,GMI Cloud 依托英伟达 GPU 平台,可提供稳定可控的性能、弹性扩容的算力以及高性价比的运行服务。GMI Cloud 打造了全栈垂直整合的人工智能基础设施体系,覆盖推理接口、调度编排、算力计算至硬件设备的全链路环节。  关于 Karmada 用户组  Karmada 用户组是一个由在其环境中成功采用 Karmada 的组织和用户组成的社区。作为连接社区与用户的核心纽带,Karmada 用户组致力于打造一个深度融合、开放协作的高价值平台,推动成员间的高效联动与经验共享,助力云原生多云多集群生态系统的蓬勃发展。成为 Karmada 用户组成员具有以下优势:社区认可:作为云原生多集群管理领域的领导者来展示您的组织,在 CNCF 和 Karmada 社区中获得知名度;协作与交流:与其他采用者建立联系,分享最佳实践,并在实际用例和解决方案上进行协作;保持更新:及时接收重要更新通知,包括关键功能、错误修复和安全建议;活动参与:受邀参与 Karmada 相关活动,包括 KubeCon + CloudNativeCon、网络研讨会和聚会;职位发布:有机会在 Karmada 社区支持的职位公告板上发布与 Karmada 相关的职位空缺(暂不可用);扩展商业机会:与 Karmada 生态系统的其他成员建立潜在的商业联系和合作。您可以在 GitHub 社区仓库中了解更多关于 Karmada 用户组[2] 的信息, 并在 karmada.io/adopters [3] 查看完整的公开的采用者列表。截至目前,Karmada 用户组已吸纳来自全球的 40+ 家机构和组织。更多使用场景及案例研究请查阅:https://karmada.io/adopters   加入 Karmada 用户组   Karmada 用户组对当前正在生产环境中使用 Karmada 的最终用户和供应商开放。这包括:最终用户:在其生产环境中运行 Karmada 的组织;供应商:提供基于 Karmada 的产品或服务,并有客户在生产环境中使用这些产品或服务的公司。您是否在生产环境中使用 Karmada 并有兴趣加入 Karmada 用户组?访问下方 Karmada 用户组申请表单 [4],提交 issue 申请,即可接收申请进度。手机端可扫描下方二维码快捷填写申请表单。扫码申请加入用户组更多信息,请访问:[1] GMI Cloud: https://www.gmicloud.ai/[2] Karmada 用户组: https://github.com/karmada-io/community/tree/main/adopter-group[3] Karmada 采用者列表: http://karmada.io/adopters[4] Karmada 用户组申请表单: https://github.com/karmada-io/community/issues/new?template=adopter-group-application.yamlKarmada Adopter Group 欢迎您的加入!期待与您共同创建一个友好而活跃的空间,共享知识、最佳实践和经验,为企业与社区发展缔造更多可能。如需了解更多关于 Karmada Adopter Group 的信息,请联系:Maintainer Mailing Listcncf-karmada-maintainers@lists.cncf.io Karmada(https://karmada.io/)是 CNCF 首个跨云跨集群容器编排引擎,由华为云、工商银行、小红书、中国一汽等八家企业联合发起。Karmada的贡献企业与贡献者遍布全球 22 个国家和地区的 100 多个组织,包括华为、道客、浙江大学、腾讯、滴滴、Bloomberg、Yunhorn、携程等。截至目前,项目在 GitHub 上已获得 5.2k+Star。 关注魔方公众号,获取更多前沿资讯添加社区小助手k8s2222,进入技术交流群
  • [热门活动] Karmada 社区夏季远程带薪实习申请中,欢迎加入 LFX Mentorship 2026
    由 Linux Foundation 组织的 LFX Mentorship 计划[1],从2019年开始为 CNCF 各个开源社区中的开发人员持续提供带薪实习和指导。该项目往年已获 2w+ 申请,发起 2000+ 课题,毕业实习生1400+,发放超过 380 万美金报酬。LFX Mentorship 2026 夏季(Term 2)申请时间为5月5日 – 5月19日(11:00 PDT;18:00 UTC),远程实习将从 6 月 8 日开始为期三个月。参与到 LFX Mentorship 计划中,为开源项目做贡献、获得开源社区的认可同时,完成工作还能获取报酬 (位于中国的开发者报酬为 3000 美金,约合 20000 人民币)。Karmada 社区在 LFX Mentorship 计划的课题申请正在火热进行中,感兴趣的开发者即日起可前往官方平台申请:https://mentorship.lfx.linuxfoundation.org/  Karmada 社区介绍  Karmada 是 CNCF 首个多云多集群容器编排项目。作为开放的多云多集群容器编排引擎,Karmada 旨在帮助用户在多云环境下部署和运维业务应用。凭借兼容 Kubernetes 原生 API 的能力,Karmada 可以平滑迁移单集群工作负载,并且仍可保持与 Kubernetes 周边生态工具链协同。Karmada 社区在 GitHub 上 Star 超过5.6k+,贡献者广泛分布于 20+国家和地区,现已在华为云、道客、兴业数金、中国移动、中国联通、携程、360集团、新浪、中通快递等众多企业单位生产应用,为企业提供从单集群到多云架构的平滑演进方案。社区地址:cid:link_0  面向对象  本期计划申请者需 2026 年 5 月 19 日前在 LFX 官网[1]完成 Mentee 注册及项目申请。若被接收作为 Mentee,您将能在开源社区经验丰富、积极贡献的 Mentor 指导下为开源项目做出贡献。依据官方规定,对 Mentee 申请者有以下要求[2]:在参加该计划时您至少年满 18 周岁所在单位和组织不禁止该实习未参加另外的 Linux Mentorship 计划开发者以个人身份参与(在校或已毕业均可)具备所注册国家中工作权利且所注册国家未被计划禁止 (中国已获许可)满足具体所属项目中提及的其它前置需求  课题参与方式  根据官方安排 [3],LFX Mentorship 2026 年 Term2 申请及实习流程如下:Mentee 注册与项目申请:5月5日-5月19日(11:00 AM PDT/18:00 UTC)申请者审核期: 5月20日-6月2日(11:00 AM PDT/18:00 UTC)申请者入选通知:6月3日实习正式开始:6月8日中期考核:7月21日(11:00 AM PDT/18:00 UTC)首次津贴支付:7月22日结项考核、实习报告提交:8月25日(11:00 AM PDT/18:00 UTC)最终津贴支付批准:8月26日本期结束最终日期:8月31日参与者如有意向如何申请?流程详见 [4]:https://docs.linuxfoundation.org/lfx/mentorship/mentee-guide/how-to-apply实习申请结果预计将在 6 月 3 日通知到申请人。主线开发日期为 2026 年 6 月 8 日-8 月 25 日,全程线上协作,无需线下参与。结项需要在 2026 年 8 月 25 日前以 PR 的形式提交到项目所在的开源社区仓库中并完成合并。  Karmada 项目课题  在 LFX Mentorship 2026 Term 2,Karmada社区的课题如下:▍Karmada: Build Agent Skills课题描述:Karmada 是一个多集群编排系统,当前的 AI 编程智能体尚未形成与该系统深度匹配的技能结构。在缺乏专用技能库的情况下,智能体往往需要依赖零散的文档、在不充分的上下文中推断策略行为,这可能导致生成的PropagationPolicy、OverridePolicy或排查建议不够准确。本课题旨在为 AI 编程智能体系统性地构建面向 Karmada 的技能集。整体设计沿用成熟的“多技能库”模式,包含共享知识库、面向工作流的专项技能,以及用于处理格式敏感任务的确定性辅助工具。预期产出:一个共享知识库,涵盖策略 API、策略模式、故障排查案例、组件指南以及可复用的示例。用于策略生成、策略评审、放置解释、传播调试以及多集群管理的确定性辅助脚本、测试夹具和示例场景。面向贡献者的文档,说明如何在 hack/agent-skills/ 下新增技能、示例和知识文件。前置技能:Go; Kubernetes; React; CI/CD Systems ( GitHub Actions); Document tools课题导师:Hongcai Ren(@RainbowMango )qdurenhongcai@gmail.comZhen Chang (@XiShanYongYe-Chang )changzhen5@huawei.com课题申请入口:https://mentorship.lfx.linuxfoundation.org/project/12821d1d-9528-4cf3-ac3c-d57a6b585599对课题内容有任何问题,欢迎直接向课题导师发送邮件或在GitHub仓库提交Issue提问。扫码回复“Karmada” 进入技术群今年夏季,Karmada社区期待在 LFX Mentorship 见到您!附:申报指引[1] LFX Mentorship计划官网及申报入口: https://mentorship.lfx.linuxfoundation.org/#projects_all[2] LFX Mentorship - Application Requirement: https://docs.linuxfoundation.org/lfx/mentorship/mentee-guide/am-i-eligible[3] LFX Mentorship - Program Readme: https://github.com/cncf/mentoring/blob/main/programs/lfx-mentorship/2026/02-Jun-Aug/README.md[4] LFX Mentorship - Mentee Application Guideline: https://docs.linuxfoundation.org/lfx/mentorship/mentee-guide/how-to-apply
  • [技术干货] 从零开始理解大模型(六):训练——70 亿个参数是怎么"学"出来的
    欢迎阅读「从零开始理解大模型」系列 —— 十篇文章,从"下一个词预测"到完整的大模型心智模型。每篇配可运行代码。第一篇:一切从"猜下一个词"开始第二篇:Token——大模型眼中的"字"长什么样第三篇:向量与 Embedding——把文字变成数学第四篇:Attention——大模型的"阅读理解"机制第五篇:Transformer 全景——积木怎么搭成大厦第六篇:训练——70 亿个参数是怎么"学"出来的第七篇:推理——你按下回车后的这一秒发生了什么第八篇:上下文窗口——大模型的"工作记忆"第九篇:Scaling Law——为什么"大力出奇迹"有效第十篇:从大模型到 Agent——下一个词预测如何长出手脚* 本系列配套运行代码,可在公众号后台回复“大模型”完整获取。作者:十一前五篇讲的是大模型的"身体结构"——Token、Embedding、Attention、FFN 怎么组装在一起。但结构只是骨架,参数才是灵魂。一个刚初始化的模型,所有参数都是随机数。问它 "Thank you very" 后面是什么?完全瞎猜——50257 个 token 概率差不多。训练之后呢?99.2% 的概率输出 "much"。从随机到精准,中间发生了什么? ▍一、先说结论 一句话版本:训练 = 喂数据 → 预测 → 算误差 → 调参数 → 重复几万亿次。▍二、训练的核心循环想象你在调一台有 70 亿个旋钮的收音机。目标是调到某个电台(让预测尽可能准),但你不知道每个旋钮该转到什么位置,只能靠试:随便拧 —— 给所有旋钮一个随机初始位置听一下 —— 用当前参数预测下一个词对答案 —— 看预测和真实文本差多远(算 Loss)微调旋钮 —— 每个旋钮稍微转一下,方向是让差距变小重复 —— 换下一段文本,再来一轮重复几万亿次。每次只调一点点,但积累下来,70 亿个旋钮就逐渐到了正确的位置。下面把每一步展开。 ▍三、Loss——“猜得有多离谱”模型输出 50257 个概率。训练时我们知道正确答案,所以可以量化"猜得有多差"——这个数叫 Loss(损失)。3.1 具体例子输入 "Thank you very",正确答案是 "much"。 随机初始化的模型 训练好的模型"much" 的概率: 1/50257 ≈ 0.002% 0.992 (99.2%)3.2 Loss 的计算公式Loss = -log(P(正确答案))代入数字:随机模型: Loss = -log(1/50257) = 10.8 ← 很大,猜得很离谱训练好的: Loss = -log(0.992) = 0.008 ← 接近 0,猜得很准为什么用负对数?两个好处:概率从 0 到 1,取负对数后变成 0 到正无穷——数值范围更适合做优化猜得越离谱(概率越小),Loss 越大,惩罚力度和"离谱程度"成正比你不需要记公式,只需记住:Loss 是一个数字,越小越好。训练的目标就是让它不断变小。 ▍四、梯度——“每个旋钮该往哪边转”知道了 Loss(猜得有多差),下一个问题是:70 亿个参数,每个该往哪个方向调?调多少?4.1 梯度是什么回到收音机。你现在 Loss 是 10.8(信号很差)。把某个旋钮稍微往右转一点点,听听 Loss 变了没:变成 8.3 → 往右转是对的,继续变成 8.7 → 方向反了,应该往左梯度就是这个信号:告诉每个参数"往哪个方向调"以及"调多大步"。 数学上,梯度是 Loss 对每个参数的偏导数。但不需要理解偏导数——只要知道它给出了方向和幅度。4.2 反向传播——从输出往回推模型的计算方向是:输入 → Embedding → Layer 1 → Layer 2 → ... → Layer 12 → 输出 → Loss梯度的计算方向是反过来的:Loss → 输出 → Layer 12 → ... → Layer 2 → Layer 1 → Embedding从 Loss 出发,通过链式法则,把"该怎么调"的信号一路传回每一层的每一个参数。这叫反向传播(Backpropagation)。就像多米诺骨牌倒着推——Loss 推倒最后一块,连锁反应一路传到第一块。每个参数都收到了自己该怎么调的信号。4.3 梯度下降——一步一步走下山有了梯度,就可以更新参数:新参数 = 旧参数 - 学习率 × 梯度学习率(learning rate) 是一个很小的数(比如 0.0001),控制每步走多远。太大会跨过最优解来回震荡,太小训练太慢。整个过程就像在一个 70 亿维的山谷里找最低点:梯度告诉你哪边是下坡,学习率决定每步多远,一步一步走,最终走到(接近)谷底——也就是 Loss 最小的地方。 ▍五、代码实验:亲手训练一个微型模型GPT-2 太大,没法在笔记本上训练。附件 train_tiny.py[1] 实现了一个微型 Transformer——2 层、4 个头、128 维,在 10 句话上训练,CPU 几分钟跑完。训练循环的核心只有四行,所有大模型的训练都是这四行:# 1. 前向传播:用当前参数做预测logits = model(x)# 2. 算 Loss:预测和正确答案差多远loss = F.cross_entropy(logits.view(-1, vocab_size), y.view(-1))# 3. 反向传播:算出每个参数的梯度(该往哪调)loss.backward()# 4. 更新参数:沿梯度方向微调一步optimizer.step()不管模型有 10 万参数还是 1 万亿参数,训练循环都是这四步。差别只在每一步的计算量。运行效果 Step 0: Loss = 83.250 (0.1s) Step 50: Loss = 2.395 (0.5s) ████████████ Step 100: Loss = 1.767 (0.8s) ████████████████ Step 150: Loss = 1.311 (1.0s) ████████████████████ Step 200: Loss = 0.976 (1.3s) ██████████████████████ Step 250: Loss = 0.834 (1.7s) ███████████████████████ Step 300: Loss = 0.527 (2.0s) ██████████████████████████ Step 350: Loss = 0.312 (2.3s) ███████████████████████████ Step 400: Loss = 0.263 (2.6s) ████████████████████████████ Step 450: Loss = 0.264 (2.8s) ████████████████████████████ Step 499: Loss = 0.219 (3.1s) ████████████████████████████训练后测试: 输入: 'The capital of ' → 输出: 'The capital of Italy is Rome.' ✓ 输入: 'Thank you very' → 输出: 'Thank you very much for your help.' ✓这说明了什么Loss 从 83.250 降到 0.219——模型从瞎猜变成了能续写训练数据中的句子。前 100 步下降最快(容易的规律先学到),后面越来越慢(难的规律需要更多迭代)。但它只是在"背课文"——我们只喂了 10 句话,模型记住了这 10 句话的模式。问它训练数据里没有的内容,它就会胡说。真正的大模型在几万亿个 token 上训练,覆盖了互联网上的海量文本。所以它"记住"的不是具体句子,而是语言的统计规律——语法结构、事实知识、推理模式。数据量的差异,决定了"背课文"和"学会语言"的区别。完整可运行代码见附件 train_tiny.py,纯 PyTorch 实现,不需要 transformers 库。* 本系列配套运行代码,可在公众号后台回复“大模型”完整获取。 ▍六、真实的大模型训练:三个阶段我们的微型模型只经历了一个阶段。真实的大模型要经过三个阶段,每个阶段解决不同的问题。6.1 预训练(Pre-training)——“广泛读书”目标:让模型学会语言——语法、知识、推理能力。数据:几万亿个 token 的互联网文本——网页、书籍、论文、代码、维基百科……任务:就是我们讲过的"预测下一个词"。把文本切成片段,每个位置预测下一个 token,算 Loss,调参数。规模感受:预训练完成后,模型已经能生成流畅的文本,知道大量事实,能做简单推理。但它有个问题:不会好好回答问题。你问它一个问题,它可能不回答,而是继续生成看起来像网页的内容——因为在训练数据里,一段文本后面跟的通常是另一段文本,不是"回答"。6.2 SFT(监督微调)——“入职培训”目标:教模型"好好回答问题"。数据:人工编写的高质量问答对,通常几万到几十万条:用户:法国的首都是哪里?助手:法国的首都是巴黎。巴黎位于法国北部...用户:写一个 Python 斐波那契函数助手:def fibonacci(n): if n <= 1: return n return fibonacci(n-1) + fibonacci(n-2)训练方式:和预训练完全一样——预测下一个 token,算 Loss,调参数。只是数据换成了对话格式,模型就学会了"看到问题后给出回答"的模式。SFT 的数据量比预训练小得多(几万条 vs 几万亿 token),但它让模型从"续写机器"变成了"能回答问题的助手"。6.3 RLHF(基于人类反馈的强化学习)——“师傅带徒弟”目标:让回答更符合人类偏好——更有帮助、更安全、更诚实。SFT 教模型"怎么回答",RLHF 教模型"什么样的回答更好"。分两步做:第一步:训练一个"打分模型"。同一个问题让模型生成两个回答 A 和 B,人类标注员选更好的那个。用大量这种偏好数据训练一个打分模型,让它能自动给回答打分。第二步:用打分模型优化语言模型。模型生成回答 → 打分模型打分 → 分数作为奖励信号 → 调整参数让模型倾向于生成高分回答。6.4 三阶段的效果变化预训练前: "jk2#x..." ← 随机噪声预训练后: "...the city is known for..." ← 流畅续写,但不会回答问题SFT 后: "巴黎是法国的首都。" ← 能回答了,但质量参差不齐RLHF 后: "法国的首都是巴黎。巴黎位于法国北部..." ← 回答详细、有帮助、安全▍七、训练的工程挑战原理不复杂,但真把一个 70B 模型训出来,工程上很难。7.1 显存装不下一个 7B 模型的训练需要多少显存?参数本身: 7B × 4 字节 = 28 GB 梯度: 同样大小 = 28 GB 优化器状态: Adam 需要额外存两份 = 56 GB 激活值: 中间计算结果 = 数十 GB ────────────────────────────── 总计: 远超 100 GB一张 A100 GPU 有 80 GB 显存,连参数和优化器都装不下——一张卡根本没法训 7B 模型。解决办法是分布式训练:把模型切成几块,分到不同的 GPU 上,几千张 GPU 通过高速网络协同工作。这就是为什么训大模型要花数百万美元——大部分是算力成本。7.2 数据质量比数据数量更重要训练数据里如果充满错误信息、恶意内容或低质量文本,模型就会学到这些"坏习惯"。所以训练前要做大量清洗:去重(互联网上到处是重复内容)、过滤垃圾页面、去除隐私信息、平衡语言和领域的比例。数据配比(多少英文、多少中文、多少代码、多少数学)是各家模型团队的核心秘方。 ▍八、训练改变了什么回顾前五篇,训练到底改变了哪些东西:Embedding 表(第三篇):训练前是随机数。训练后,"France" 和 "Paris" 的向量变得接近,"国王 - 男人 + 女人 ≈ 女王" 自动形成。Attention 的 Q/K/V 权重(第四篇):训练前,注意力分数是随机的,每个词对所有词的关注度差不多。训练后,"very" 学会了重点关注 "Thank you" 这个搭配。FFN 的权重(第五篇):训练前,FFN 做的是随机变换。训练后,FFN 学会了存储和调用知识——看到 "Thank you very" 就激活 "much"。所有这些变化,都来自同一个训练信号:预测下一个词。 没有人告诉模型 "France 的首都是 Paris",也没人告诉它 "Thank you very 后面接 much"。它只是在几万亿次预测中自动发现了这些规律,编码进了参数里。 ▍九、结语训练的本质简单得令人意外:预测 → 算误差 → 调参数 → 重复。复杂的不是算法,而是规模——70 亿个参数、几万亿个 token、数千张 GPU、数月时间。把一个简单过程推到极致的规模,涌现出了看起来像"理解语言"的能力。"Deep learning is just gradient descent on a loss function. Everything else is engineering."深度学习就是在 Loss 上做梯度下降。剩下的全是工程。下一篇,我们从训练切换到使用——你按下回车后的这一秒,模型内部发生了什么?为什么第一个字等得久、后面快了?KV Cache 是什么?本文配套代码:train_tiny.py(微型 Transformer 从零训练)。纯 PyTorch 实现,不需要 transformers 库,CPU 几分钟跑完。扫码回复“大模型”获取本系列文章完整配套代码「从零开始理解大模型」是「从零开始理解 Agent」的姊妹系列。Agent 系列讲"四肢",本系列讲"大脑"。建议对照阅读 专栏入口。
  • [热门活动] 远程实习+3000美金!Volcano社区夏季实习申请中,欢迎加入LFX Mentorship 2026
    由Linux Foundation组织的LFX Mentorship计划[1],从2019年开始为CNCF各个开源社区中的开发人员持续提供带薪实习和指导。该项目往年已获2w+申请,发起2000+课题,毕业实习生1400+,发放超过380万美金报酬。LFX Mentorship 2026 夏季(Term 2)申请时间为5月5日 – 5月19日(11:00 PDT;18:00 UTC),远程实习将从 6 月 8 日开始为期三个月。参与到LFX Mentorship计划中,为开源项目做贡献、获得开源社区的认可同时,完成工作还能获取报酬 (位于中国的开发者报酬为3000美金,约合20000人民币)。Volcano社区在LFX Mentorship计划的课题申请正在火热进行中,感兴趣的开发者即日起可前往官方平台申请(在校/在职符合条件可):https://mentorship.lfx.linuxfoundation.org/  Volcano 社区介绍  Volcano 是 CNCF 首个批量计算项目,也是业界领先的云原生统一调度平台。Volcano 已从批量调度引擎演进为通智融合调度系统,面向 AI 训练、大模型推理、Agentic AI、大数据、基因测序等多样化工作负载提供统一的高性能调度能力,对 Spark、Flink、Ray、TensorFlow、PyTorch、Kubeflow、MPI、PaddlePaddle、MindSpore 等主流计算框架均有完善支持。社区在GitHub上已获得 6.5k+ Star 和 1.3K+ Fork,参与贡献企业包括华为、AWS、百度、腾讯、京东、小红书、bilibili 等。社区地址:🌋 https://github.com/volcano-sh在LFX Mentorship 2026夏季计划,Volcano期待与你协作开拓AI大数据、LLM大模型推理等场景调度的更多可能。  面 向 对 象 本期计划申请者需2026年5月19日前在LFX官网[1]完成Mentee注册及项目申请。若被接收作为Mentee,您将能在开源社区经验丰富、积极贡献的Mentor指导下为开源项目做出贡献。依据官方规定,对Mentee申请者有以下要求[2]:在参加该计划时您至少年满18周岁所在单位和组织不禁止该实习未参加另外的Linux Mentorship计划开发者以个人身份参与(在校或已毕业均可)具备所注册国家中工作权利且所注册国家未被计划禁止 (中国已获许可)满足具体所属项目中提及的其它前置需求  课题参与方式  根据官方安排 [3],LFX Mentorship 2026 年 Term2 申请及实习流程如下:Mentee注册与项目申请:5月5日-5月19日(11:00 AM PDT/18:00 UTC)申请者审核期: 5月20日-6月2日(11:00 AM PDT/18:00 UTC)申请者入选通知:6月3日实习正式开始:6月8日中期考核:7月21日(11:00 AM PDT/18:00 UTC)首次津贴支付:7月22日结项考核、实习报告提交:8月25日(11:00 AM PDT/18:00 UTC)最终津贴支付批准:8月26日本期结束最终日期:8月31参与者如有意向如何申请?流程详见 [4]:https://docs.linuxfoundation.org/lfx/mentorship/mentee-guide/how-to-apply实习申请结果预计将在6 月 3 日通知到申请人。主线开发日期为2026年6月8日-8月25日,全程线上协作,无需线下参与。结项需要在2026年8月25日前以 PR 的形式提交到项目所在的开源社区仓库中并完成合并。  Volcano 项目课题  欢迎各位申请者申报CNCF Volcano社区课题:▍支持 Namespace 级别的 Queue课题描述:Volcano 当前的 Queue 是集群级别的资源,只有集群管理员才有权限创建和修改。这在多租户场景下对普通租户不太友好:租户通常只拥有自己 namespace 下的权限,但也想用上 Queue 提供的能力(资源共享、capability/guarantee/deserved、队列层级等)来管理自己的作业,而不是每改一次都要依赖集群管理员。本课题会在 Volcano 中引入 namespace 级别的 NamespaceQueue。NamespaceQueue 从 cluster 级别的 Queue 派生,语义和使用方式与 cluster Queue 保持一致,租户可以直接在自己的 namespace 下创建和管理队列,并像使用 cluster Queue 一样把 PodGroup/Job 关联到 NamespaceQueue 上进行调度。对不使用这个特性的用户,原有 cluster Queue 的行为和 API 完全不受影响。预期产出:新增 namespace 级别的 NamespaceQueue CRD,从 cluster 级别的 Queue 派生,capability/guarantee/deserved、队列层级等字段语义保持一致租户不需要集群管理员权限,就能在自己 namespace 下创建和管理 NamespaceQueuePodGroup/Job 支持引用 NamespaceQueue,调度行为与引用 cluster Queue 一致NamespaceQueue 的资源统计、status、事件等能够正常工作与原有 cluster Queue 和 scheduling.volcano.sh/queue-name annotation 兼容,老用户可以平滑迁移覆盖核心流程和异常场景的 E2E 测试Volcano 官网和代码仓库的用户文前置技能:GoKubernetes(CRD、controller、RBAC)熟悉 Volcano(scheduler、queue、PodGroup/Job 等)E2E 测试(Ginkgo)GitHub workflow 与 shell 脚课题导师:Zicong Chen(@JesseStutler )jessestutler97@gmail.comHajnal Máté (@hajnalmt)hajnalmt@gmail.comJoão Azevedo (@devzizu)jazevedo960@gmail.com原始Issue:cid:link_2/issues/5251课题申请入口:https://mentorship.lfx.linuxfoundation.org/project/9e582ac4-4e12-4fcc-a1cd-27855d79730a▍Kthena Router 支持第三方模型 API课题描述:目前 Volcano/Kthena Router 已经能够很好地支持集群内部的路由能力。但在与用户的交流中,部分用户提出希望能够对接外部的大语言模型 API。因此,Kthena 社区计划借助本期 LFX,让 Router 具备访问第三方 LLM API 的能力。预期结果:设计提案(Proposal)代码实现(包含单元测试;如能补充端到端测试更佳。但考虑到外部 LLM API 通常不会提供长期、稳定的免费访问,端到端测试不作为强制要求)用户指南及相关文前置技能:Go、Kubernetes、网络、LLM课题导师:Zengzeng Yao(@yaozengzeng )yaozengzeng@huawei.com原始Issue:cid:link_3/issues/939课题申请入口:https://mentorship.lfx.linuxfoundation.org/project/baa6fb1f-58c9-4255-849c-21aca35ce5e1▍Kthena Router 性能基准测试(Benchmark)课题描述:Kthena Router 是 Volcano/Kthena 项目中的 LLM 路由组件,负责将推理请求转发到集群内(以及即将支持的)第三方模型后端。随着 Kthena 逐步走向成熟,社区需要一套可复现的基准测试,用来在真实的 LLM 流量模式下衡量 Router 的性能特征(吞吐、延迟、TTFT、资源占用),并在版本迭代中及时发现性能回退。本项目希望设计并实现一套面向 Kthena Router 的基准测试框架,产出完整的测试报告。如果在过程中发现明显的瓶颈或可优化点,还需要与 Maintainer 协作,将代码层面的优化合入上游。预期产出:一套可复用的 Kthena Router 基准测试框架(压测客户端、场景配置、指标采集、结果汇总),可在本地乃至 CI 中运行。一组覆盖典型 LLM 路由模式的测试场景(不同的 QPS、prompt/response 长度、并发数、后端数量、路由策略等)。端到端测试流程文档化为 runbook(集群准备、mock/真实后端、运行方式、结果解读)。一份基准测试报告,包含吞吐、TPOT、TTFT、GPU/CPU/内存等指标,以及瓶颈分析。针对识别到的明显瓶颈进行优化并提交上游 PR,附上优化前后的性能对比数据前置技能:Go、Kubernetes、性能基准测试与性能剖析(pprof)、对 LLM 推理及 HTTP/gRPC 路由有基本了解。课题导师:Zengzeng Yao(@yaozengzeng )yaozengzeng@huawei.comZhonghu Xu (@hzxuzhonghu )zhhxu2011@gmail.com原始Issue:cid:link_3/issues/942课题申请入口:https://mentorship.lfx.linuxfoundation.org/project/1cf34cb5-3520-4f15-81c8-4258cb9abcb9▍支持多 AgentCube 协同能力课题描述:目前, Volcano社区AgentCube项目 在 Code Interpretation和 Agent Runtime场景下都只会拉起单个 Agent。然而,当前我们已经进入多 Agent 协作的时代。因此,AgentCube 计划支持多 Agent 编排,使多个 Agent 能够围绕同一个任务进行协作,并由 AgentCube 统一管理这些 Agent 的生命周期。预期结果:设计提案(Proposal)代码实现(包含单元测试和端到端测试)用户指南及其他相关文前置技能:Go、Kubernetes、Agent课题导师:Zhonghu Xu (@hzxuzhonghu )zhhxu2011@gmail.com原始Issue:cid:link_1/issues/301课题申请入口:https://mentorship.lfx.linuxfoundation.org/project/9e7dd1c1-5d14-4c5e-af43-83454099e490更多Volcano课题,请访问LFX Mentorship官网[1];对课题感兴趣/有任何问题,欢迎直接向课题导师发送邮件或在GitHub仓库提交Issue提问。今年夏季,Volcano社区期待在 LFX Mentorship 见到您! 附:相关指引链接[1] LFX Mentorship计划官网及申报入口: https://mentorship.lfx.linuxfoundation.org/#projects_all[2] LFX Mentorship - Application Requirement: https://docs.linuxfoundation.org/lfx/mentorship/mentee-guide/am-i-eligible[3] LFX Mentorship - Program Readme: cid:link_0[4] LFX Mentorship - Mentee Application Guideline: https://docs.linuxfoundation.org/lfx/mentorship/mentee-guide/how-to-apply[5] Volcano GitHub: cid:link_2[6] Volcano/Kthena GitHub: cid:link_3[7] Volcano/AgentCube GitHub: cid:link_1 关注魔方公众号,获取更多前沿资讯添加社区小助手k8s2222,进入Volcano技术交流群
  • [技术干货] 普惠 DeepSeek-V4:Kthena + 昇腾 3 分钟搭建 PD 分离推理
    一、背景介绍在大模型推理部署中,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 v0.4.0 正式发布:进一步简化 LLM 大模型工作负载管理
    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” 进入技术群 
  • [热门活动] 从零开始理解大模型(五):Transformer 全景——积木怎么搭成大厦
    欢迎阅读「从零开始理解大模型」系列 —— 十篇文章,从"下一个词预测"到完整的大模型心智模型。每篇配可运行代码。第一篇:一切从"猜下一个词"开始第二篇: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  
  • [技术干货] 从零开始理解大模型(四):Attention——大模型的"阅读理解"机制
    欢迎阅读「从零开始理解大模型」系列 —— 十篇文章,从"下一个词预测"到完整的大模型心智模型。每篇配可运行代码。第一篇:一切从"猜下一个词"开始第二篇: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 系列讲"四肢",本系列讲"大脑"。建议对照阅读 专栏入口。
  • [技术干货] 从零开始理解大模型(三):向量与 Embedding——把文字变成数学
    欢迎阅读「从零开始理解大模型」系列 —— 十篇文章,从"下一个词预测"到完整的大模型心智模型。每篇配可运行代码。第一篇:一切从"猜下一个词"开始第二篇: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——大模型眼中的"字"长什么样
    欢迎阅读「从零开始理解大模型」系列 —— 本系列十篇文章,从"下一个词预测"到完整的大模型心智模型。每篇配可运行代码。第一篇:一切从"猜下一个词"开始第二篇: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(番外篇):Token 都花在哪了?
    「从零开始理解 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:
总条数:215 到第
上滑加载中