Files
AutoZero/llm/types.go
T
Sakurasan d5dac5a6fc feat: AutoZero 通用 Agent 框架(从 AutoGo 五阶段拆分完成)
llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、
builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、
编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。

产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt)
留在 AutoGo,宿主接入方式见 MIGRATION.md。
2026-10-26 00:54:46 +08:00

217 lines
10 KiB
Go
Raw 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 llm 定义统一的 LLM Provider 抽象(借鉴 pigo internal/provider 的
// 统一层思路),当前支持 OpenAI 兼容与 Anthropic 两种协议,均为手写 SSE 流式。
package llm
import (
"context"
"strings"
)
// Role 取值:system / user / assistant / tool。
type Role string
const (
RoleSystem Role = "system"
RoleUser Role = "user"
RoleAssistant Role = "assistant"
RoleTool Role = "tool"
)
// ThinkingLevel 是每轮请求的思考/推理深度。注意零值是 ""(不是 ThinkingOff)。
type ThinkingLevel string
const (
ThinkingOff ThinkingLevel = "off"
ThinkingLow ThinkingLevel = "low"
ThinkingMedium ThinkingLevel = "medium"
ThinkingHigh ThinkingLevel = "high"
)
// Enabled 仅对 low/medium/high 为真;off 与零值 "" 均为 false。
// 判定务必用本方法,不要写 `!= ThinkingOff`(零值不等于 off)。
func (l ThinkingLevel) Enabled() bool {
return l == ThinkingLow || l == ThinkingMedium || l == ThinkingHigh
}
// ParseThinkingLevel 归一化输入;空值与未知值一律回退 ThinkingOff。
func ParseThinkingLevel(s string) ThinkingLevel {
switch ThinkingLevel(strings.ToLower(strings.TrimSpace(s))) {
case ThinkingLow:
return ThinkingLow
case ThinkingMedium:
return ThinkingMedium
case ThinkingHigh:
return ThinkingHigh
default:
return ThinkingOff
}
}
// IsThinkingLevel 报告 s 是否为四个合法值之一(配置校验用)。
func IsThinkingLevel(s string) bool {
v := ThinkingLevel(strings.ToLower(strings.TrimSpace(s)))
return v == ThinkingOff || v.Enabled()
}
// StreamOptions 是单轮请求的可选参数。零值 = off:不发送任何新字段。
type StreamOptions struct {
Thinking ThinkingLevel
// SystemDynamic 是不该进入 prompt-cache 断点的 system 后缀:计划、记忆、
// 回退分支摘要这类每轮可能变化的内容。Anthropic 把它作为第二个 system 块
// (第一个块带 cache_control);OpenAI 兼容/Responses 追加在同一条 system
// 消息尾部——token 前缀缓存仍命中稳定段,同时避免部分网关不接受多条
// system 消息。
SystemDynamic string
// DisableTools 保留 tools 定义、但禁止模型调用(三家的 tool_choice=none)。
// 收尾回合用它而不是把 tools 传成 nil:换掉工具数组会打碎缓存前缀。
DisableTools bool
}
// ToolCall 是模型发起的一次工具调用请求。
type ToolCall struct {
ID string `json:"id"`
Name string `json:"name"`
Input string `json:"input"` // 原始 JSON 参数字符串
}
// Image 是随消息发送的图片(按文件名引用,构建请求时从缓存读取并 base64 编码;
// 会话 JSONL 只存文件名,不存像素)。
type Image struct {
Filename string `json:"filename"` // 缓存中的文件名
Path string `json:"path,omitempty"` // 兼容旧格式:直接路径
}
// Message 是会话中的一条消息。ToolCallID 非空表示这是工具结果回填。
type Message struct {
Role Role `json:"role"`
Content string `json:"content"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
Images []Image `json:"images,omitempty"` // 常见于截图类工具结果
Usage *Usage `json:"usage,omitempty"`
// Reasoning 是开启思考时的推理文本(展示用;Anthropic 场景还需回传,见下)。
Reasoning string `json:"reasoning,omitempty"`
// ReasoningSignature 是 Anthropic thinking 块的签名:工具调用场景下,
// 下一轮请求必须原样回传 thinking 块(含签名),否则 API 会拒绝。
ReasoningSignature string `json:"reasoning_signature,omitempty"`
// RedactedThinking 是 Anthropic 的 redacted_thinking 数据(安全策略触发时),
// 同样需要原样回传。
RedactedThinking string `json:"redacted_thinking,omitempty"`
// --- 本地 trace:仅为度量与展示,绝不进入任何 API 请求报文 ---
// 三家 provider 都手工白名单拼 body(oaBuildMessages / rsBuildInput /
// anBuildMessages),全仓不存在 json.Marshal(Message),故这些字段不会出网;
// 它们只随会话 JSONL 落盘。omitempty 保证旧消息与旧格式输出不变。
StartedAt int64 `json:"started_at,omitempty"` // 本步开始时刻(unix ms)
DurationMs int64 `json:"duration_ms,omitempty"` // 本步耗时:assistant = 模型调用,tool = 工具执行
Step int `json:"step,omitempty"` // 第 N 次模型调用(tool 消息沿用其所属轮次)
// Format/Model 是这一步的归因:协议格式 + 实际模型名。
// 没有它们就分不清"哪一步换了供应商/模型"——真实会话里出现过一条中途
// 切了 provider 的记录,事后只能靠 tool_call id 的前缀形态猜。
Format string `json:"format,omitempty"`
Model string `json:"model,omitempty"`
// Tool 是 tool 消息对应的工具名。此前只有 ToolCallID 落盘,工具名得从失败块
// 文本里 parse(retro.parseFriction 一直在这么干)。
Tool string `json:"tool,omitempty"`
// Fail 是归一出的失败类别(timeout / denied / self-kill / …),空 = 成功。
// 有了它,stats 与 retro 不必再靠「[TOOL FAILED] …(kind)」字符串前缀计数——
// 那条契约的原话是"内容前缀是唯一可靠信号",格式漂一处就静默失准。
Fail string `json:"fail,omitempty"`
// Exit 是命令类工具的退出码(取不到就不序列化)。
Exit int `json:"exit,omitempty"`
// Truncated 表示 Content 被长度上限截过:模型看到的不是全量,UI 该标出来,
// 统计也知道这条会话里有多少结果其实被削了。
Truncated bool `json:"truncated,omitempty"`
// OutputLimited 是"**模型想写却写不下**":这一轮的终止原因是输出 token 上限
// (OpenAI finish_reason=length / Anthropic stop_reason=max_tokens /
// Responses response.incomplete + max_output_tokens)。
//
// 与 Truncated 正相反:那是我们自己削工具结果,这是模型的笔停了。不接住它的后果
// 是"讲一半就停"——agent 的收尾判定是"没有 tool_calls ⇒ 说完了",于是半截答案
// 被当成最终回复,用户只能自己打「继续」。带 tool_calls 的响应一律不算(见 completion)。
OutputLimited bool `json:"output_limited,omitempty"`
// Interrupted 是"这条没写完就断了":模型调用出错或被停止时,已经流出来的半截
// 回复仍值得留下(见 agent.persistPartial),但它**不是**一个答案。
// 没有这个标记,会话末尾看起来就像已经答完了,重试入口只能回一句
// "没有可重试的消息"——而用户明明只看到一个断掉的半句。
Interrupted bool `json:"interrupted,omitempty"`
// Synthetic 标记这条 user 消息是系统生成的(后台作业完成通知等),不是人打的字。
// 只用于本地展示与统计:provider 的手工白名单拼报文不会带上它;统计、rewind
// 提问号与标题派生据此跳过,避免把通知算成一次人工干预。
Synthetic bool `json:"synthetic,omitempty"`
}
// ToolDef 向模型声明一个可调用工具。
type ToolDef struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters map[string]any `json:"parameters"` // JSON schema
}
// Usage 记录一次响应的 token 消耗。
//
// **口径约定:InputTokens 是本次请求在模型侧占位的提示 token 总量**,缓存命中
// 与缓存写入都算在内,Cache* 字段只是明细。必须这么归一:Anthropic 的
// `input_tokens` 只报**未命中缓存**的那一段(缓存部分单列在
// cache_read_input_tokens / cache_creation_input_tokens)。照抄的后果是终端计数、
// `stats` 的 TokensIn 与任何按 token 算的成本都被低报(95% 命中时 10 万的上下文
// 记成 5 千)。注意:当前自动压缩走的是**字符估算**(见 agent/context.go),
// 不依赖这几个数,所以这个口径修的是账目,不是压缩触发条件。
// OpenAI / Responses 的 prompt_tokens 本身已含缓存,取其明细即可。
type Usage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
// CacheReadTokens 是命中提示缓存的 token 数(Anthropic cache_read /
// OpenAI prompt_tokens_details.cached_tokens)。
CacheReadTokens int `json:"cache_read_tokens,omitempty"`
// CacheCreationTokens 是本次写入缓存的 token 数(Anthropic 独有,计费更贵)。
CacheCreationTokens int `json:"cache_creation_tokens,omitempty"`
// ReasoningTokens 是思考消耗(OpenAI/Responses 单列;Anthropic 混在
// output_tokens 里报不出来,留 0,不要瞎猜)。
ReasoningTokens int `json:"reasoning_tokens,omitempty"`
}
// StreamDelta 是流式回调收到的一段增量内容。ToolCall 是一个工具调用的参数
// 已经完整的提前通知:调用方可以在消息结束前先执行只读工具;最终
// Message.ToolCalls 仍包含完整列表,按 ID 去重即可。
type StreamDelta struct {
Text string
Reasoning string
ToolCall *ToolCall
Usage *Usage
Err error
}
// Provider 是统一的 LLM 后端接口。
type Provider interface {
// Stream 发起一轮对话,增量回调 delta 流;返回最终的 assistant 完整消息。
// opts 为零值时不改变请求(思考级别默认关闭)。
Stream(ctx context.Context, system string, msgs []Message, tools []ToolDef, opts StreamOptions, onDelta func(StreamDelta)) (Message, error)
}
// ModelSwitcher 是支持运行时切换模型的 Provider。
type ModelSwitcher interface {
SetModel(model string)
}
// CurrentModeler 是能报告当前模型的 Provider。模型预设路由(含图走多模态、
// 失败回落备选)需要先知道现状才能决定切不切、以及回合后恢复到哪——
// 只读查询,与 SetModel 配对使用。
type CurrentModeler interface {
CurrentModel() string
}
// Identifier 是 provider 自报「协议格式 + 当前模型」的能力(三家都实现)。
// Agent 用它给每条 assistant 消息盖归因戳:会话中途切了供应商或模型时,
// 没有这两项就分不清哪一步是谁做的。
type Identifier interface {
Identity() (format, model string)
}
// ModelLister 是支持枚举可用模型的 Provider(GET /models)。
type ModelLister interface {
ListModels(ctx context.Context) ([]string, error)
}