budget

package
v0.31.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 11, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package budget 上下文预算:输入侧的**分段计量**、窗口上限计算与压实计划骨架。

为什么需要这个包

本仓已有 spill,但它管的是**输出**超预算(生成结果太长怎么办)。输入侧此前完全 缺位:下一轮要重发的会话历史、系统提示、工具面各占多少,没有任何一个包回答。 缺的不是「一个总数」,而是三件分别的事:

  1. **分段**:只给总数时超预算就只有一个动作——砍历史。而工具面膨胀、规则膨胀 与历史膨胀是三种不同的病,处置完全不同。
  2. **上限**:窗口不是全归输入的,输出得先占位,否则下一次请求生下来就已截断。
  3. **处置**:到了上限要做什么。本包只给**压实**(保留语义、丢弃逐字)的计划 骨架,摘要怎么生成、原文往哪落盘各留一个接口由宿主实现。

纪律

内核**不调 LLM、不碰存储、不猜 tokenizer**:本包只做确定性计量与计划, 摘要生成走 Summarizer、原文归档走 Archive,宿主实现。

与 spill 的分工:spill 管输出溢出后的截断与取回,本包管输入面计量与压实, 语义不同不互相替代——本包不提供任何溢出文本的落盘能力。

压实之后怎么把账改回来

压实改写了会话段,但**没有产生模型调用**,所以下一轮到来之前没有任何新报数可依。 三步走完整闭环,全部在本包内:

m := budget.Meter{}; m.Add(budget.SegmentConversation, 60000)
p, _ := budget.BuildPlan(budget.PlanInput{Items: 10, KeepTail: 4, ItemTokens: 2500})
res, _ := budget.Apply(ctx, p, leading, tail, arc, sum)
m.Shrink(budget.SegmentConversation,
    budget.NetRemovedTokens(res.GrossRemovedTokens, res.Summary.Tokens))

毛移除量与净移除量必须分开取(NetRemovedTokens 有专门注释):按名字对齐参考实现的 同名字段会静默错账,方向恰好是「看起来更省了」。

一条不可让步的度量纪律

分段估算**永不回填** provider 报数。理由:provider 报的是总数,回填到分段就等于 让 tokenizer 误差披上分段精确度的外衣——UI 上「工具面 3.2k」看起来是真的, 实际是「总数减其余各段」的残差。压实计划恰恰要按段分配裁剪,喂残差等于喂假账。

Index

Constants

View Source
const (
	// ForceKeepItems 强制保留尾部条数(压实的**设计基线**)。
	//
	// 数值取自参考实现:强制压实时至少逐字留下这么多条。内核取这个值不是因为
	// 「6」有魔法,而是因为它同时是**第一条守卫的推导来源**——见 MinEffectiveItems。
	ForceKeepItems = 6

	// MinSummarizeItems 最小的待摘要批量。摘要的价值在**跨条目归纳**,
	// 只摘要 1 条拿不到归纳,只会产出一段更长的复述。
	MinSummarizeItems = 2
)

压实下界(拒绝「看起来能跑、实际没干活」的操作)。

三条都是**空操作守卫**:压实在边界上会退化成什么都不做,而「什么都不做却报成功」 比报错更糟——上下文照旧超限,宿主还以为已经处置过了。

View Source
const (
	// TightAtPct 进入 tight 档的占用率。
	TightAtPct = 75
	// CriticalAtPct 进入 critical 档的占用率。
	CriticalAtPct = 90
)

压力阈值(百分比整数,不用浮点:浮点在 0.9 这种边界上会因舍入把 89% 判成 tight 之外的档,判定必须可复现)。

View Source
const MinEffectiveItems = ForceKeepItems + MinSummarizeItems

MinEffectiveItems 压实所需的最少有效条目数 = ForceKeepItems + MinSummarizeItems。 对齐参考实现的 MIN_EFFECTIVE_MESSAGES = FORCE_KEEP_MESSAGES + 2。

为什么阈值必须挂在保留尾部基线上,而不是一个独立常量

参考实现没有取「至少 2 条」这种独立常量,而是由**强制保留的尾部条数**加上待摘要 下限推出来。这不是写法风格,而是防绕过:独立常量下,调用方把 keepTail 调成 1 就能 把守卫拆掉——3 条历史压成 1 条摘要,看起来是在处置,实际只是把内容换了个说法再赔上 一份摘要的 token。阈值挂在保留尾部基线上,把 keepTail 调小**不会**让门槛跟着降, 守卫就再也绕不过去。

为什么不是「按调用方的 keepTail 派生」

刻意**不**收调用方的 keepTail 参数:门槛一旦跟着调用方的 keepTail 走(哪怕取下限), 「过了门槛」就恒等价于「cutoff >= 2」,下面两条守卫会变成**数学上不可达的死代码**—— 而留着死守卫等于给「越界不会被发现」一个假的安全感。参考实现同样把两者分开:门槛取 模块常量 FORCE_KEEP_MESSAGES,而 cutoff 取当次实际算出的位置(自动压实会按比例取更 大的保留量),正因如此 cutoff<=0 与「待摘要不足 2 条」才仍是**活的**兜底。

这里的 + MinSummarizeItems 也正是这两条活守卫能存在的余地。

View Source
const MinInputTokens = 1

MinInputTokens 输入侧上限的**地板值**,只在「窗口 - 输出预留」真的算成非正时兜底。

为什么地板是 1 而不是报错:算出 <=0 时真正的病灶在宿主配置(窗口给小了,还是 输出预留要大了),内核猜不出是哪一条,就不该替宿主定性一个配置错误。让它返回 1 的后果是这次请求几乎必然溢出——而溢出是**看得见**的信号:展示口径 Overflow 为真、 Classify 判 critical。反之抛错只会把「窗口小」和「预留大」两种病因混成一个错误。

注意地板**不作用于**「没预留」的早退分支:宿主没要求预留时说「给 1」是在凭空 发明一个它没要求的约束,那比让它拿到自己写的上限更糟(见 MaxInput)。

View Source
const SummaryItemID = "summary"

SummaryItemID 压实后摘要条目在会话段中的 ID。

Variables

View Source
var (
	// ErrPlanMismatch 入参条目与计划对不上(计划已过期或调用方算错了下标)。
	// 宁可不压:按过期计划摘要,会把该保留的尾部内容一起吞掉。
	ErrPlanMismatch = errors.New("压实拒绝:入参条目与计划不符(计划已失效)")

	// ErrEmptyPlan 计划没有指定任何待摘要条目(零值 Plan,或 Cutoff/待摘要数为 0)。
	//
	// 为什么不并入 ErrPlanMismatch:两条病因的现场不同——过期计划是「会话已经长了」,
	// 空计划是「压根没打算处置」(宿主建计划失败后拿了零值继续跑)。按同一条理由
	// 合并成一种,等于让宿主看到 ErrPlanMismatch 却在错误的地方找原因。
	ErrEmptyPlan = errors.New("压实拒绝:计划未指定任何待摘要条目(空计划压不出东西)")

	// ErrPlanInconsistent 计划各字段互不自洽(Cutoff 与待摘要数不等、或
	// 待摘要+保留尾部不等于条目总数)。
	//
	// 自洽的计划由 BuildPlan 产出且只有一种构造方式;手搓出矛盾字段的计划,
	// 其 Cutoff 只是一个谎——Apply 真正用的是另外两个数,于是「按计划压实」变成
	// 「按一份自相矛盾的回执压实」,事后无法复核。宁可拒绝。
	ErrPlanInconsistent = errors.New("压实拒绝:计划各字段互不自洽")

	// ErrNotWired 执行压实所需接口未装配。
	ErrNotWired = errors.New("压实拒绝:归档 / 摘要接口未装配")
)

压实执行期的拒绝原因。

View Source
var (
	// ErrTooFewItems 有效条目数不足,连一次压实都撑不起来。
	ErrTooFewItems = errors.New("压实拒绝:有效条目数低于最小压实体量")

	// ErrCutoffTooLow 截断下标非正,即保留的尾部会吞掉整段会话。
	ErrCutoffTooLow = errors.New("压实拒绝:截断下标非正,保留尾部会吞掉整段会话")

	// ErrTooFewToSummarize 待摘要条目数不足最小摘要批量。
	ErrTooFewToSummarize = errors.New("压实拒绝:待摘要条目数低于最小摘要批量")
)

压实拒绝原因。调用方应按 errors.Is 判定并把 Detail 呈给用户, 而不是把它当布尔吞掉——「为什么不压实」和「压实了」是两种不同的现场。

Functions

func MaxInput

func MaxInput(window, baseInput, outputReserve int) int

MaxInput 输入侧可用上限。对齐参考实现的 input_token_budget: 无预留或窗口未知时**原样返回自设上限**,否则返回 min(baseInput, max(window-outputReserve, MinInputTokens))。

为什么要扣掉输出预留

窗口是输入输出**共享**的。若拿整个窗口当输入上限,请求是「装得下」的, 下一轮模型却在生成到一半时被截断——而那时用户看到的现象是「回答说到一半没了」, 与上下文超限完全无关,排查方向会被带偏。预留先扣,截断就不会发生在生成为零的时候。

为什么没有预留就原样返回(而非仍然相减再取地板)

参考实现在这里直接早退,理由是「没要求预留」与「预留算出来不够」是两种不同的现场: 前者是宿主**没有**这项约束,扣减纯属内核自作主张,后者才是宿主约束与窗口打架。 早退还顺带保证了自设上限宽于窗口时不会被内核单方面收窄——那属于宿主的显式决定。

参数取值的含义

  • window:模型上下文窗口总长;<=0 视为未知。
  • outputReserve:输出预留(宿主按任务形态定);<=0 视为不预留。
  • baseInput:调用方自设的输入上限;<=0 表示「没有自设上限」,此时只由窗口推导。

地板只在**确实扣成非正**时兜底(预留吃光窗口 / 预留超过窗口 / 全零输入)。

func MaxInputWithHostCap

func MaxInputWithHostCap(window, baseInput, outputReserve, hostCap int) int

MaxInputWithHostCap 在 MaxInput 之上让**宿主显式配置**的提示上限直接生效。

为什么需要这条独立入口

参考实现里,显式上限与推导上限不是同一个数被反复比较,而是**分岔**的:宿主一旦 在配置里写死了提示上限就直接用,不再与「窗口 - 预留」取小。若把它塞进 MinInput 里,这条显式决定会被窗口值悄悄推翻,而推翻它的那一方(内核)没有任何依据。

hostCap > 0 时直接返回它,**不做任何窗口比较也不取地板**:显式配置不是推导值, 地板保护只服务于推导路径。

func NetRemovedTokens

func NetRemovedTokens(gross, summaryTokens int) int

NetRemovedTokens 压实的**净**移除量 = max(0, 毛移除量 - 摘要量),对齐参考实现的 removed_tokens 口径。

为什么这个换算是显式函数而不是字段上的减法

宿主在拿到 Result 之前**没有**摘要量(摘要由宿主自己生成),所以毛移除量与净移除量 不可能同时存在于结构体里。把换算写成一个函数,是为了让「毛」与「净」在类型和字段名 上就分得开——同名字段分处两地时,按名字映射两份实现的宿主会静默错账,方向恰好是 「看起来多省了一份摘要」,而多省出来的量根本不存在。

钳位在 0:摘要比被移除的原文还长时,净移除量是 0 而不是负数。负数会让调用方 反向加账(凭空给上下文添 token),而压实本来只可能让上下文变小。

Types

type Archive

type Archive interface {
	// Archive 落盘压实前的原文并返回取回句柄。
	Archive(ctx context.Context, items []Item) (Locator, error)
}

Archive 原文归档出口(宿主实现)。

压实是不可逆的逐字丢弃,所以原文必须先落地:摘要是**有损**的,事后发现摘要 丢了关键信息时,唯一能救回来的地方是这份归档。内核不碰存储,只定义这次接缝。

type Display

type Display struct {
	// Input 未钳位的输入量(分段估算或 provider 报数,取决于哪个 Display)。
	Input int
	// Cap 展示用上限;<=0 表示**未知上限**,此时 Used 直接返回 Input。
	Cap int
}

Display 展示口径。

结构上只有 Input 与 Cap 两个来源字段,Used 由二者派生而**不可直接赋值**—— 这样「used = min(input, cap)」就不可能与 input/cap 对不上。

func (Display) Overflow

func (d Display) Overflow() bool

Overflow 输入是否已越界(仅在设了上限时有意义)。

func (Display) Used

func (d Display) Used() int

Used 展示值 = min(Input, Cap)。未设上限时返回 Input。

type Item

type Item struct {
	// ID 条目标识(宿主分配,须能回到原始内容)。
	ID string
	// Tokens 该条估算 token。
	Tokens int
}

Item 会话段条目引用:内核只认**标识与规模**,不携带内容。

内容归宿主(必要时凭 Locator 取回)。内核搬运内容就等于替宿主选定了存储形态, 那正是本仓不做的事——与 contract.Fields 开放键、checkpoint.Fields 同一条纪律。

type Locator

type Locator string

Locator 归档定位符(取回句柄)。格式归宿主,内核只当不透明串传递。

type Meter

type Meter struct {
	// contains filtered or unexported fields
}

Meter 分段计量器。零值可用,并发安全(编排可能从多个环节同时计量)。

func (*Meter) Add

func (m *Meter) Add(seg Segment, tokens int)

Add 累加一段的估算 token。

负值按 0 计入:估算器给出负数是**估算侧的 bug**,把它原样累加等于允许一次 负计量悄悄抹掉别段的量,而总数偏低时没人看得出。宁可少算也不要假装精确。

空段名不做特殊处理:照样计量、照样出现在报告里。总数因此永远不会「少算」, 一个无名分段出现在报告尾部是诚实的信号,吞掉它才是假的。

func (*Meter) Reset

func (m *Meter) Reset()

Reset 清零(新一轮计量)。保留 map 复用——计量每轮都调,不必每次重新分配。

func (*Meter) Segments

func (m *Meter) Segments() []SegmentUsage

Segments 按固定顺序列出:规范分段在前,未知(宿主)分段按名字排序在后。

未计量过时返回的是**规范分段的零值列表**(六条 Tokens=0),不是空切片—— 调用方遍历不必判空,且「这一段存在但量为 0」本身就是一句诚实的陈述。 快照同理(见 Snapshot):规范段永远出现,未计量就是 0。

func (*Meter) Shrink

func (m *Meter) Shrink(seg Segment, tokens int) int

Shrink 从一段**就地减去** tokens 个 token,返回实际扣掉的量。对齐参考实现的 shrink_context_usage:压实改写了下一轮提示但**没有产生新的模型调用**,所以在下一 轮到来之前不会有任何新报数回来,持有快照的一方若不减,账面会一直停在压实前的旧值。

为什么必须存在这条路径

压实计划给出的是净移除量(见 NetRemovedTokens),没有对应的减法入口就等于让调用方 把减法做在别处:或另记一份账(两份真相必然漂移),或干脆不减(快照虚高,且虚高的 方向恰好让界面显示「还在紧张」,指向一个已经不存在的现场)。

钳位与可观测性

单段扣减按**该段实有量**钳位,不会倒扣到别的段:把会话段压过头不应该连带抹掉 系统提示段的计量。返回的**实际**扣减量因此可能小于入参——返回值 < 入参即为钳位, 调用方据此能分辨「账上就这么多」与「真扣了这么多」,而不用自己去比 Total。

非正入参是空操作并返回 0:扣除负数等于反向加账。

func (*Meter) Snapshot

func (m *Meter) Snapshot(cap int) Report

Snapshot 取报告快照。cap 传 0 表示「本轮没有窗口约束」,展示口径按未知上限处理。

func (*Meter) Tokens

func (m *Meter) Tokens(seg Segment) int

Tokens 单段当前值;未计量过返回 0。

func (*Meter) Total

func (m *Meter) Total() int

Total 各段求和(内核唯一的输入真相)。

type Plan

type Plan struct {
	// Items 计划基于的条目总数(宿主据此判计划是否已过期)。
	Items int
	// Cutoff 截断下标:下标 < Cutoff 的条目进摘要,[Cutoff, Items) 逐字保留。
	Cutoff int
	// SummarizeItems 进摘要的条目数(等于 Cutoff)。
	SummarizeItems int
	// KeepTailItems 逐字保留的条目数(等于 Items-Cutoff)。
	KeepTailItems int
	// GrossRemovedTokens 压实时被逐字移除的领先条目估算 token;无 ItemTokens 时为 0。
	//
	// 字段名里的 Gross 是**硬要求**,不是修饰词:参考实现里同名的 removed_tokens 是
	// **净值** max(0, 移除量 - 摘要量),而这里是扣掉摘要之前的**毛移除量**。同名不同义,
	// 宿主按名字对齐两份实现时会静默错账——账面凭空少掉一份摘要的量,且方向恰好是
	// 「看起来更省了」。要净额请用 NetRemovedTokens,不要改名字求和。
	GrossRemovedTokens int
}

Plan 压实计划:保留尾部逐字内容、领先条目进摘要,并给出压实前移除量的估算。

计划是**纯计算产物**,不含内容也不做 I/O:宿主拿它决定要不要压实、 按什么次序执行(先归档后摘要,见 Apply)。

func BuildPlan

func BuildPlan(in PlanInput) (Plan, error)

BuildPlan 由会话段条目数与保留尾部算出压实计划。纯函数。

三条空操作守卫(按序判定,各自可独立触发)

  1. ErrTooFewItems:有效条目数 < MinEffectiveItems。整段没到能压的体量。 阈值挂在保留尾部基线上,见该常量注释——这是「不能被 keepTail 绕过」的那一条。
  2. ErrCutoffTooLow:截断下标 <= 0。保留的尾部吃掉整段,压实后仍是满的, 只是把「超时」换成了「更慢地超时」。
  3. ErrTooFewToSummarize:待摘要条目数 < MinSummarizeItems。宁可不动, 也不做「摘要 1 条」这种看起来在工作、实则是复述的假处置。

为什么返回原因而不是 bool

三条守卫都指向同一个表面症状(压不下去了),处置却不同:等一轮能压、 调小保留尾部、还是这条会话已经不值得压。布尔会把三种病因压成一种。

type PlanInput

type PlanInput struct {
	// Items 会话段当前条目数(逐条可寻址的单位:一条消息 / 一次工具往返)。
	Items int
	// KeepTail 尾部逐字保留条数;负值按 0 视作「不保留尾部」。
	KeepTail int
	// ItemTokens 单条估算 token(用于估 GrossRemovedTokens);<=0 则不估。
	ItemTokens int
}

PlanInput 压实计划入参。

type Pressure

type Pressure int

Pressure 上下文压力档位。

const (
	// PressureNormal 余量充足,不需要处置。
	PressureNormal Pressure = iota
	// PressureTight 接近上限,该准备压实了。
	PressureTight
	// PressureCritical 已越界或余量不足,本轮大概率截断。
	PressureCritical
)

func Classify

func Classify(input, capacity int) Pressure

Classify 按占用率判定压力档。纯函数,无副作用。

边界取「到达即进档」

阈值处判**上一档之外更高的一档**:占用率正好等于 90% 时,下一轮几乎必然溢出, 若把它算作 tight,最后一个来得及处置的时点就被抹掉了。宁可早一档提示。

上限未知即 critical

capacity <= 0 说明调用方没给出窗口约束,此时**不能声称余量充足**—— 「不知道会不会溢出」和「不会溢出」在行为上完全相反。

func (Pressure) String

func (p Pressure) String() string

String 档位名(留痕与事件载荷用)。

type Report

type Report struct {
	// Segments 分段明细,规范分段在前。
	Segments []SegmentUsage
	// Input 各段求和。
	Input int
	// Cap 展示用上限(见 MaxInput);<=0 = 未知上限。
	Cap int
	// Provider provider 自报的输入总量;<=0 表示本次无报数。
	Provider int
}

Report 一次计量报告:分段估算、provider 报数、展示口径三者**分列**。

刻意不给「用 provider 报数覆盖 Input」的入口:分段估算一旦被回填, 压实计划按段分配裁剪就建立在残差上(见包注释的度量纪律)。

func (Report) Display

func (r Report) Display() Display

Display 分段估算的展示口径(钳位在显示层做,估算值原样保留)。

func (Report) Drift

func (r Report) Drift() int

Drift provider 报数与分段估算之差(正 = 估算偏低)。只用于**观测**估算器的偏差, 不参与任何裁剪决策。

func (Report) ProviderDisplay

func (r Report) ProviderDisplay() Display

ProviderDisplay provider 报数的展示口径;本次无报数时退回分段估算口径, 免得 UI 在无报数时显示一个凭空而来的 0。

type Result

type Result struct {
	// Segment 压实后的会话段:一条摘要 + 逐字保留的尾部。
	Segment []Item
	// Summary 摘要原文(宿主决定是否回填到展示层)。
	Summary Summary
	// Locator 原文归档句柄(供后续取回)。
	Locator Locator
	// GrossRemovedTokens 本次逐字移除的估算量(取自计划),**未扣掉摘要**。
	//
	// 与参考实现的 removed_tokens(净值)同名不同义,务必走 NetRemovedTokens 取净额。
	GrossRemovedTokens int
}

Result 压实执行结果。

func Apply

func Apply(ctx context.Context, p Plan, leading, tail []Item, arc Archive, sum Summarizer) (Result, error)

Apply 按计划执行压实:先归档原文,再生成摘要,最后拼出压实后的会话段。

顺序是刻意定的:归档必须先于摘要

摘要失败(超时、限流、模型返回空)时原文已经落盘,可以直接重试;反序则可能出现 「摘要已丢弃原文、摘要本身又没拿到」的净损失。不可逆操作之后的那一步,必须最便宜。

计划自身的三条前置校验(在归档之前,全部无副作用)

  1. ErrEmptyPlan:待摘要条目数 <= 0。零值 Plan 能一路通过「入参条数与计划相等」 这道校验(0 == 0),于是产出「1 条摘要 + 0 条尾部」和一条成功回执——上下文 一字未动,宿主却以为已经处置过。本包对空操作的态度写在 BuildPlan 的常量注释里: **报成功而什么都没做,比报错更糟**。
  2. ErrPlanInconsistent:Cutoff≠待摘要数,或 待摘要+保留尾部≠条目总数。 Cutoff 真正驱动的是「哪一段进摘要」,它与被实际使用的两个数字对不上时, 这份计划不可复核。
  3. ErrPlanMismatch:入参条数与计划对不上——计划已过期。既有语义,不变。

三条都在任何归档之前判定:被拒的调用不得留下任何副作用。

内核做什么、不做什么

编排(校验 → 归档 → 摘要 → 拼接 → 给修正量)是代码,所以归内核; 生成摘要与落盘是能力,所以归宿主(Summarizer / Archive)。本函数里没有任何 模型调用与文件操作,这正是本包存在的意义。

func (Result) SnapshotDelta

func (r Result) SnapshotDelta() int

SnapshotDelta 内存快照的**带符号修正量**(加到已记录的会话段 token 上)。

要「从快照里减掉多少」请直接用 NetRemovedTokens —— 本方法给的是带符号值, 摘要比原文还长时为正(表示要加回去),方向与「压实」直觉相反,容易用错。 压实后的正确用法:m.Shrink(段, NetRemovedTokens(res.GrossRemovedTokens, res.Summary.Tokens))。

type Segment

type Segment string

Segment 上下文分段种类。

取值是 string 而非闭合枚举:**宿主自定义分段直接用自己的键**即可,内核不解释、 不校验、不假设它们存在(与 contract.Fields 的开放键同口径)。 下列常量是内核认得的**规范分段**——内核对它们的处理是通用的,但给名字便于 不同宿主对齐口径,也便于测试夹具对齐语义。

const (
	// SegmentSystem 系统提示段(每轮固定重发)。
	SegmentSystem Segment = "system"
	// SegmentTools 工具定义面(本轮实际可调用的工具声明)。
	SegmentTools Segment = "tools"
	// SegmentRules 规则 / 约束段(每轮注入的行为约束)。
	SegmentRules Segment = "rules"
	// SegmentSkills 技能说明段。
	SegmentSkills Segment = "skills"
	// SegmentMCP MCP 服务面(远端工具服务的能力声明)。
	SegmentMCP Segment = "mcp"
	// SegmentConversation 会话历史段(唯一随轮次持续增长的段)。
	SegmentConversation Segment = "conversation"
)

type SegmentUsage

type SegmentUsage struct {
	Segment Segment
	Tokens  int
}

SegmentUsage 单段计量结果。

type Summarizer

type Summarizer interface {
	// Summarize 把领先条目压成一条摘要。leading 已由 Apply 校验过条数与计划一致。
	Summarize(ctx context.Context, leading []Item) (Summary, error)
}

Summarizer 摘要生成出口(宿主实现)。

内核**不调 LLM**:「编排是代码,LLM 只做单环节」——摘要是一次 LLM 调用, 属于模型面,不属于编排面。内核给计划、宿主给摘要,接口边界即纪律边界。

type Summary

type Summary struct {
	Text   string
	Tokens int
}

Summary 摘要结果。Tokens 由宿主如实填写:内核只把它加回账,不核实、不修正。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL