大模型打分与采样原理,以及 Pi Agent 核心原理
本文分三部分:
- 大模型如何根据用户提示词给"自己认识的词"打分(logits 与 softmax)
- temperature 与 top-p 到底改变了什么
- Pi Agent(pi.dev,Mario Zechner / Earendil Inc.)的核心架构原理
第一部分:大模型如何给词打分
1.1 一句话结论
大模型本质上是一个"给词表里每一个候选词打分"的函数。给定已有的上下文(系统提示词 + 用户提示词 + 已生成的部分),模型输出一个长度等于词表大小的分数向量,再把这些分数转成概率分布,最后从分布里"抽"一个词出来,追加到上下文尾部,重复这个过程。
用公式表达就是:
P(xt∣x1,x2,...,xt−1)=softmax(zt),zt∈R∣V∣
其中 zt 就是 logits(未归一化的原始分数), ∣V∣ 是词表大小(常见规模 3 万到 20 万)。
1.2 "自己认识的词"是什么:词表与 Tokenizer
模型并不认识"字"或"单词",它只认识 token(子词单元)。Tokenizer(通常是 BPE 或 SentencePiece)在训练前就固定下来了一份词表,比如:
text
id 15496 -> "Hello"
id 11 -> ","
id 1917 -> " world"
id 30 -> "?"
中文一般 1 个汉字对应 1 到 2 个 token,英文一个常见单词往往是 1 个 token。词表是封闭集合:模型永远只能在这份固定的 token 列表里打分,这就是"模型认识的所有词"。词表之外的内容会被拆成更小的碎片(极端情况拆到字节级),所以模型不会"不认识",只会"拆得更碎"。
1.3 从提示词到分数:完整前向流程
1.4 关键步骤解析
Embedding(认识词的向量表示)
词表里每个 token 对应一个可训练的 d 维向量( d 常见 2048 到 8192)。语义相近的 token,其向量在空间中距离更近。这一步把离散符号变成连续数值。
自注意力(理解提示词内部的关系)
每个 token 生成 Query、Key、Value 三个向量,然后计算:
Attention(Q,K,V)=softmax(dk QK⊤+M)V
QK⊤ 是 token 之间的相关性打分矩阵, M 是因果掩码(保证第 t 个位置看不到未来的内容)。这一步的作用是:让"最后一个位置"的隐状态汇聚整段提示词中与预测下一词最相关的信息。提示词写得越明确,注意力权重就越集中在有效约束上,输出分布也就越尖锐。
LM Head(真正的打分环节)
最后一层隐状态 h∈Rd 与输出矩阵 W∈R∣V∣×d 相乘:
zi=Wi⋅h
zi 就是"第 i 个 token 的得分"。很多模型的 W 与输入 embedding 权重共享(weight tying),所以这一步可以直观理解为:把当前语义向量与词表里每个词的向量做点积,谁最像谁得分高。
1.5 一个可手算的例子
假设词表只有 5 个 token,提示词是"今天天气真",模型给出的 logits 为:
text
"好" -> 6.0
"热" -> 5.2
"冷" -> 4.5
"香" -> 0.3
"编译" -> -2.1
softmax( T=1)后的概率约为:
text
"好" -> 0.55
"热" -> 0.25
"冷" -> 0.12
"香" -> 0.004
"编译" -> 0.0002
注意几点:
- logits 是相对量,整体加减一个常数不改变 softmax 结果
- 分数差 1.0 大约意味着概率相差 e≈2.7 倍
- 语法上不可能的词("编译")会得到极低分,这是模型在预训练中学到的统计规律,不是规则引擎判断
1.6 logits 在 softmax 前还可能被改写
工程实现里,logits 出来后往往不是直接 softmax,而是先经过一系列 logits processors:
其中"结构约束遮罩"是 Function Calling 与 JSON 模式能稳定输出的关键:把所有不符合语法的 token 的 logit 直接置为 −∞,softmax 后概率变成 0,模型物理上不可能选到它。
第二部分:temperature 与 top-p
2.1 temperature:改变分布的"陡峭程度"
带温度的 softmax:
pi=∑j=1∣V∣exp(zj/T)exp(zi/T)
T 只做一件事:在 softmax 之前把所有 logits 同时除以 T。
- T→0+:分布退化为 one hot,等价于 greedy decoding,永远选最高分的词,输出完全确定
- T=1:使用模型训练时的原始分布
- T>1:logits 被压缩,高低分差距缩小,分布变平,低概率词有更大机会被选中
- T→∞:趋近于在整个词表上均匀随机,输出变成乱码
沿用上面的例子:
text
token T=0.2 T=1.0 T=2.0
"好" 0.982 0.55 0.36
"热" 0.018 0.25 0.24
"冷" 0.0006 0.12 0.17
"香" ~0 0.004 0.07
"编译" ~0 0.0002 0.03
关键认知:temperature 不会让模型"更聪明"或"更有创意",它只是改变了从同一份打分结果里抽样的随机性强度。 分数排序永远不变,变的只是低分词被抽中的概率。
2.2 top-p(核采样 Nucleus Sampling):动态截断候选集
top-p 的做法是:
- 把所有 token 按概率降序排列
- 从高到低累加概率
- 累加和首次达到或超过 p 时停止,前面这些 token 构成"核"(nucleus)
- 丢弃核之外的所有 token,对核内概率重新归一化
- 在核内采样
V(p)=argSmin{∣S∣:i∈S∑pi≥p}
top-p 与 top-k 的区别 :top-k 固定候选数量(比如永远取前 50),top-p 让候选数量随分布形状自适应。
- 模型很确定时(比如代码里
func后面接函数名,或者"""闭合),最高概率可能就有 0.95,此时 p=0.9 的核里只有 1 个 token,等价于贪心,避免了 top-k 强行引入 49 个垃圾候选 - 模型不确定时(比如写一段散文的开头),概率分散,核里可能有几百个 token,保留了多样性
这就是 top-p 通常优于 top-k 的原因:它对"模型自信程度"是自适应的。
2.3 两者的协同关系
temperature 与 top-p 作用在流水线的不同环节,不是二选一:
顺序很重要:temperature 先改形状,top-p 再按改完的形状截断。所以高 temperature 加低 top-p,是"先把分布拉平,再只保留头部",两者会部分抵消;低 temperature 加高 top-p,则截断几乎不起作用。
长尾为什么必须切掉 :词表里有十万个 token,即使每个只有 10−5 的概率,加起来也可能占到 20% 到 30% 的概率质量。生成 500 个 token 的回答,就有相当高的累计概率至少踩中一次完全离谱的词,而 Transformer 的自回归特性会让这一个错词污染后面全部内容(俗称"一步错,步步错")。top-p 就是防止这种尾部灾难的闸门。
2.4 实践取值建议
| 场景 | temperature | top-p | 说明 |
|---|---|---|---|
| 代码生成、工具调用、结构化输出 | 0 到 0.2 | 0.9 或 1.0 | 追求确定性与可复现 |
| 事实问答、文档摘要、翻译 | 0.2 到 0.5 | 0.9 | 少量灵活性,避免机械重复 |
| 通用对话 | 0.7 到 0.8 | 0.9 到 0.95 | 主流默认值 |
| 创意写作、头脑风暴 | 0.9 到 1.2 | 0.95 | 允许发散 |
| 数据增强、多样化候选 | 1.0 以上 | 0.98 | 需要配合后置过滤 |
补充要点:
- 一般只调其中一个。同时调两个会让效果难以归因,OpenAI 官方文档也建议二者择一
- T=0 并不严格等于"完全可复现"。GPU 浮点归约顺序、批处理组合、MoE 路由都会引入抖动,同一个请求仍可能有细微差异
- 温度过低容易触发重复退化(同一句话反复输出),因为一旦进入某个高概率环路就再也跳不出来,此时需要用 repetition penalty 或 presence penalty 而不是靠升温解决
- 推理型模型(带 thinking 的模型)通常要求固定采样参数,官方推荐值不建议改动,否则会破坏思维链质量
第三部分:Pi Agent 核心原理
Pi(
@earendil-works/pi,官网 pi.dev)是 libGDX 作者 Mario Zechner 主导开发的开源终端编码 Agent,MIT 协议,TypeScript 实现。它的设计哲学与 Claude Code、Codex 这类"功能完备"路线相反,核心口号是 Primitives, not features (要原语,不要功能),以及 最小核心,最大扩展。
3.1 四层包结构
- pi-ai :抹平各家 API 的"方言"差异,提供
stream()流式与complete()同步两个高阶函数,统一处理 token 计费、成本估算、prompt caching、中止信号、结构化工具结果 - pi-agent-core:纯逻辑层,不依赖 UI,唯一职责是持有 Agent 状态并驱动 Agent Loop,可被 CLI、SDK、RPC 服务端任意消费
- pi-tui:无闪烁、保留滚动缓冲区的差异化渲染 TUI 框架
- pi-coding-agent:把上面三者串起来,提供 Interactive(TUI)、Print(stdout)、RPC(JSONL over stdin/stdout)三种运行模式
3.2 核心抽象只有三个类型
Pi 把整个 Agent 运行时解耦成"状态 · 行为注入 · 事件协议"三层,对应三个类型:
AgentMessage:内部统一的消息表示,不直接使用某家厂商的格式AgentLoopConfig:行为注入点集合(convertToLlm、transformContext、toolExecution、beforeToolCall、afterToolCall、abortController)AgentEvent:对外发射的事件协议
Agent 类本身是一个状态机,对外只有四个公共方法:
prompt():发起新一轮交互continue():从上次中断或报错处恢复abort():紧急中止waitForIdle():等待空闲
状态通过 agent.state(只读快照)暴露,messages 数组是会话持久化与恢复的唯一数据源。
3.3 Agent Loop:Agent"能做事"的本质
Agent Loop 的本质就一句话:调用模型,判断是否要用工具,用完把结果塞回上下文,再调模型,直到模型不再请求工具。
几个值得单独拿出来讲的设计:
没有 maxSteps。作者的原话是"我从来没找到需要 maxSteps 的用例,所以为什么要加"。循环靠"模型不再请求工具"自然结束。
最晚转换(Late Conversion) 。内部始终用 AgentMessage,只在真正要发 HTTP 请求的那一刻才通过 convertToLlm 转成 OpenAI 或 Anthropic 格式。好处是同一段会话历史可以在中途换模型、换厂商继续跑,即所谓 Context Handoff。
错误即状态 。报错不清空上下文,错误消息本身留在 messages 里,continue() 后模型能看到"我上次调用失败了,原因是 X",从而自己换策略。
3.4 事件系统:可观测性的工程体现
pi-agent-core 基于 Observer 模式暴露 12 种以上事件,所有事件顺序发出并同步等待监听器,这是 TUI 渲染和扩展生命周期的底层支撑:
设计原则是"拒绝黑盒 Agent":每一次工具执行、每一次 LLM 请求都必须对用户可见。
3.5 Steering 与 Follow-up:运行中动态介入
Pi 用两条优先级不同的队列实现"Agent 跑着的时候也能插话":
配合 abort() 取消,就构成了"完全可观测 + 可介入"的交互模型:用户不需要等 Agent 撞完南墙再重来。
3.6 上下文工程:Pi 的核心洞见
Pi 认为 Coding Agent 的胜负手是上下文工程,即精确控制进入模型的每一个 token。两个可覆盖函数承担全部职责:
transformContext(messages):每次 LLM 调用之前执行,负责修剪、压缩、丢弃过期的工具输出convertToLlm(messages):负责格式转换,且自定义消息类型不会泄漏给模型
对应"三大原罪"的批判:现有框架在背后偷偷注入大量 UI 上看不到的 prompt,导致开发者无法归因输出质量问题。Pi 的系统提示词刻意压到 1000 token 以内。
3.7 "刻意不做"的清单
这是 Pi 最有辨识度的部分,也是理解其原理的关键:
| 刻意不做 | 理由与替代方案 |
|---|---|
| 不做 Plan Mode | 用 PLAN.md 文件替代:可版本控制、可跨会话共享、完全可见 |
| 不做内置 Todo | 任务列表放 TODO.md,文件是最好的持久化 |
| 不支持 MCP | MCP 工具描述会占掉 7% 到 9% 的上下文窗口(Playwright MCP 一上来就 13k token)。替代方案是 CLI 工具加 README,Agent 要用什么就先读那个 README 再用 bash 调用,这才是自然的渐进式披露 |
| 不做 Sub Agent | 子 Agent 是"黑盒中的黑盒",会丢失可观测性。需要并发时通过 bash 自我调用 pi,输出仍然完整可见 |
| 不做 maxSteps | 循环自然结束 |
| 不做权限检查 | "安全措施大多是安全剧场。一旦 Agent 能写代码并运行代码,游戏就结束了。"Agent 完全可以写个 Python 脚本绕过任何文件系统沙箱。Pi 选择 YOLO by default,把隔离责任交给容器或虚拟机 |
| 不做后台 bash 管理 | 改用 tmux,长时进程的可观测性交给成熟工具 |
3.8 工具集:4 个原语撑起 90% 场景
Pi 的默认工具集极小,核心是 read / write / edit / bash 四个,编码版本额外提供 grep / find / ls。全部通过 ExecutionEnv 抽象层执行,与 Node.js 解耦,因此可以换成远程执行、容器执行。
为什么 4 个工具够了 :bash 是万能逃生舱。任何专用工具(git 操作、包管理、HTTP 请求、数据库查询)都可以表达成一条 shell 命令,而 shell 命令是 LLM 在预训练语料里见过最多的"API",成功率天然高于任何自定义 JSON schema。相比之下,每加一个专用工具,就要往上下文里塞一份 schema,挤占的是真正有用的代码空间。
3.9 扩展机制:Extension、Skills、Pi Packages
Extension 能拿到完整的 ExtensionAPI,可以注册新工具、订阅事件、注入或改写上下文,甚至替换 transformContext。Skills 则是"文档即能力"的思路:不常驻上下文,只有描述命中当前任务时才把正文读进来。
3.10 Proxy Stream:带宽优化
面向 Web 场景(浏览器到代理服务器再到 LLM),Pi 的 proxy.ts 做了一个针对性优化:服务端不 传输 partial 字段(完整的局部消息对象),只传 contentIndex 加 delta 的轻量事件,客户端通过 processProxyEvent() 逐步重建完整消息。这样流式输出的带宽从"每个 token 一份完整快照"降到"每个 token 一个增量"。
3.11 三种部署拓扑
传输层抽象把 LLM 调用实现与 Agent 逻辑解耦:
3.12 测试:Harness
Pi 提供 Harness 测试工具,用 Mock 模型加可控工具,在不启动 TUI 的情况下对 Agent 做确定性单元测试与集成测试。这里就直接呼应了第二部分:测试 Agent 必须把 temperature 设为 0 或直接用 Mock 模型,否则采样随机性会让断言不稳定。
三部分之间的关联
三个要点串起来:
- 上下文质量决定打分质量。Pi 把系统提示词压到 1000 token、拒绝 MCP 的 13k token 工具描述,本质上是在保护注意力预算,让模型的注意力集中在真正相关的代码上,从而得到更尖锐、更可靠的概率分布。
- Agent 场景必须压低随机性 。工具调用的参数一个字符错了就整体失败,所以 tool call 场景通常 temperature 取 0 到 0.2,并配合结构化约束遮罩把非法 token 的 logit 置为 −∞。
- 循环放大误差 。单次采样 1% 的出错概率,在 20 轮工具调用的循环里会累积成显著失败率。这就是 Pi 强调可观测性与 Steering 介入的工程动机:既然无法消灭概率误差,就让人能随时看见并纠偏。
参考
- Pi 官方仓库:
earendil-works/pi,官网 pi.dev - Holtzman et al., The Curious Case of Neural Text Degeneration(top-p 核采样原始论文)
- Vaswani et al., Attention Is All You Need