Files
AskaEth 3dc1232bc2
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(comments): 注释中文化 · internal/runtime/model + internal/services/backthink(54 文件,纯替换)
- 只翻注释/文档字符串:`go/scanner` 剥注释后按 token 序列+literal 逐 token 比对,**54/54 文件 SAME**;
  改动文件总行数与 HEAD 漂移 = 0(严格 1:1,无合并/拆行)
- 裁决编号与 design 节号逐字保留(03.4.1/03.4.2/03.4.3/14.4.1/14.4.5/07.4.2-4、design/04 §3.1/§3.2 等);
  `chat_template_kwargs` / `enable_thinking` / `reasoning_effort` / `thinking:{type}` 字段名与形状说明未动
- 存疑译法(已登记):`persona` 在叙事语境用「角色」、标识符语境保留 `persona`(仓库既有模块本身混用);
  `rung`→「级」;`provider` 存在「提供商/提供者」两种落笔(可按需一键统一)
- 复核:build/vet 绿 + 两包测试绿;gofmt 干净;残留英文注释 0 条
- 既有存量(未修,只报告):`model/anthropic_test.go` 612 行 >500;`applyAnthropicEvent` 115 行 >80
2026-10-04 16:27:54 +08:00

160 lines
5.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package model
// 表示 token 预算把生成切断的提供商停止/结束原因:
// Anthropic 上报 stop_reason "max_tokens",OpenAI 兼容提供商
// 上报 finish_reason "length"。两者都被原样归一化进 Response.StopReason,
// 因此链路追踪保留提供商自己的词汇。
const (
stopReasonMaxTokens = "max_tokens"
stopReasonLength = "length"
)
// 截断恢复的边界。最多允许一次带翻倍预算的重试;
// 上限防止病态预算无界增长。hint 是连重试
// 也没产出可见文本时返回的非空占位,
// 因此被截断的回合绝不会以静默空回复的形式浮出。
// 它绝不携带思考:推理不是答案。
const (
truncationRetryCap = 32768
truncationEmptyHint = "(思考过长,未能生成回复)"
)
// effectiveMaxTokens 解出一次尝试实际发送的预算:显式的
// Request.MaxTokens,否则用适配器默认值。当协议没有适配器
// 默认值时 def 可以为零,此时适用提供商默认值。
func effectiveMaxTokens(n, def int) int {
if n > 0 {
return n
}
return def
}
// retryMaxTokens 为一次性的截断重试翻倍实际生效预算,
// 上限为 truncationRetryCap。当没有什么可翻倍时(n 与 def 都是零,
// 即适用提供商默认值),它原样返回实际生效预算,从而让
// 调用方跳过那次会发出完全相同请求的重试。
func retryMaxTokens(n, def int) int {
eff := effectiveMaxTokens(n, def)
if eff <= 0 {
return 0
}
if bumped := eff * 2; bumped < truncationRetryCap {
return bumped
}
return truncationRetryCap
}
// isTruncatedStopReason 报告某个停止原因是否意味着 token 预算
// 切断了生成。
func isTruncatedStopReason(reason string) bool {
return reason == stopReasonMaxTokens || reason == stopReasonLength
}
// isEmptyBody 报告响应是否既无可见文本也无工具
// 调用。仅工具的一轮并不是空:它的空正文是正常的、属于
// 工具循环。
func isEmptyBody(r Response) bool {
return r.Content == "" && len(r.ToolCalls) == 0
}
// isTruncatedEmpty 报告某次尝试是否在产出任何可见输出之前
// 就被 token 预算切断(无文本、无工具调用)。只有这种形状
// 才触发截断重试/兜底;正常的 tool_use 回合,其正文
// 合法地为空,不会触发。
func isTruncatedEmpty(r Response) bool {
return isEmptyBody(r) && isTruncatedStopReason(r.StopReason)
}
// withEmptyBodyHint 在响应既无文本也无工具调用时填入一段
// 非空占位正文,因此被截断的回合绝不会以静默
// 空回复的形式浮出。已携带文本或工具调用的响应
// 原样返回。
func withEmptyBodyHint(r Response) Response {
if !isEmptyBody(r) {
return r
}
r.Content = truncationEmptyHint
r.Message = Message{Role: RoleAssistant, Content: []Content{{Type: ContentTypeText, Text: r.Content}}}
return r
}
// callWithTruncationRetry 执行一次非流式尝试,当第一次尝试
// 在无可见输出下被切断时,用翻倍 token 预算重试一次,
// 随后在重试仍无产出时返回非空占位。attempt 接收要发送
// 的预算。第一次尝试的上游错误会被原样返回(不重试);
// 重试失败则退回占位,
// 而不是浮出一个空回复。
func callWithTruncationRetry(maxTokens, def int, attempt func(limit int) (Response, error)) (Response, error) {
out, err := attempt(maxTokens)
if err != nil {
return Response{}, err
}
if !isTruncatedEmpty(out) {
return out, nil
}
if bumped := retryMaxTokens(maxTokens, def); bumped > effectiveMaxTokens(maxTokens, def) {
if second, retryErr := attempt(bumped); retryErr == nil {
out = second
}
}
return withEmptyBodyHint(out), nil
}
// callStreamWithTruncationRetry 是 callWithTruncationRetry 的
// 流式对位实现。attempt 接收该次尝试的预算与事件回调。
// 重试会丢弃思考事件(第一次尝试已经
// 流过它的推理,再流一次会产生重复块),
// 同时实时转发文本/工具事件;当连重试也没有可见
// 输出时,会以文本片段发出非空占位并把它作为
// 响应正文返回,因此流绝不会以静默截断收尾。
func callStreamWithTruncationRetry(
maxTokens, def int,
attempt func(limit int, onEvent func(StreamEvent)) (Response, error),
onEvent func(StreamEvent),
) (Response, error) {
out, err := attempt(maxTokens, onEvent)
if err != nil {
return Response{}, err
}
if !isTruncatedEmpty(out) {
return out, nil
}
if bumped := retryMaxTokens(maxTokens, def); bumped > effectiveMaxTokens(maxTokens, def) {
if second, retryErr := attempt(bumped, dropThinking(onEvent)); retryErr == nil {
out = second
}
}
if isEmptyBody(out) {
out = withEmptyBodyHint(out)
emitTextSegment(onEvent, out.Content)
}
return out, nil
}
// dropThinking 包裹 onEvent,使重试尝试不会再次发出思考
// 片段。文本与 tool_call 事件原样通过。
func dropThinking(onEvent func(StreamEvent)) func(StreamEvent) {
if onEvent == nil {
return nil
}
return func(ev StreamEvent) {
switch ev.Kind {
case StreamKindThinkingStart, StreamKindThinkingDelta, StreamKindThinkingEnd:
return
default:
onEvent(ev)
}
}
}
// emitTextSegment 发出一个单增量的文本片段,使合成的占位
// 能到达只渲染 message.* 事件的流式消费者。
func emitTextSegment(onEvent func(StreamEvent), text string) {
if onEvent == nil || text == "" {
return
}
onEvent(StreamEvent{Kind: StreamKindTextStart})
onEvent(StreamEvent{Kind: StreamKindTextDelta, Delta: text})
onEvent(StreamEvent{Kind: StreamKindTextEnd})
}