llm — 四协议互转与 Jev 决策
pkg/core/llm 用一份统一的请求/响应模型(canonical
Request / Response)在四种 wire 协议间互转,
并提供统一的访问入口;Client.Decide 另外走 Jev(System One)
决策问答。本示例不离开本机、不需要 API key。
运行
完全离线,是一个命令行程序,没有端口也没有 config.yaml。
示例先打印同一份请求翻译成的四种 wire body,再用进程内假上游(httptest)
跑通 Chat 协议的非流式与流式调用,最后在假 System One 上游上问一次 Jev。
go run ./examples/llm # 等价于 make example NAME=llm
它演示了什么
- 四种协议互转:
req.ToChat() / ToResponses() / ToAnthropic() / ToGemini()把一份 canonical 请求渲染成各协议的 wire body。 - 跨协议转换:
llm.Convert(body, ProtocolChat, ProtocolAnthropic)走 canonical 中转。 - 统一访问:功能选项建 client(
WithBaseURL/WithAPIKey/WithModel/WithTimeout/WithMaxTokens),Chat一次拿完整响应。 - 流式事件:
ChatStream返回一个统一事件通道 (EventText / EventToolCall / EventUsage / EventDone / EventError)。 - Jev 决策问答:
Decide回答带类型的问题——choice(从 criteria 里选一项)与score(按 bands 打分)。 - 可选重试:
WithRetry(retry.Config{Attempts: 3})只重试"稍后再试"类失败(408/409/425/429/5xx 与传输错误),400/401/403/404 立即返回——本示例不设重试,默认关。
四种协议的渲染
来自 examples/llm/main.go。同一份 canonical 请求交给四个
To* 方法,各协议的工具调用形状不同——Chat 是
tool_calls、Responses 是 function_call、
Anthropic 是 tool_use、Gemini 是 functionCall。
req := demoRequest()
// 1. 四种协议的转换:同一份请求 → 各协议的 wire body。
fmt.Println("== 一份 canonical 请求 → 四种协议 ==")
printWire("OpenAI Chat Completions", req.ToChat)
printWire("OpenAI Responses", req.ToResponses)
printWire("Anthropic Messages", req.ToAnthropic)
printWire("Google Gemini", req.ToGemini)
// 协议互转:Chat 的 wire body → Anthropic 的 wire body。
chatBody, _ := req.ToChat()
anthropic, err := llm.Convert(chatBody, llm.ProtocolChat, llm.ProtocolAnthropic)
if err != nil {
fatalf("Convert: %v", err)
}
demoRequest 刻意覆盖了文本、一次工具调用和一条工具结果——
每个协议都必须承载这三种形状。
流式调用
来自 main.go 的 ChatStream 段。事件是统一的,所以调用方不关心
上游是 SSE 还是别的分片方式;提前退出用 llm.StopStream(ch)
释放连接与 goroutine(示例是消费到底)。
ch, err := c.ChatStream(context.Background(), req)
if err != nil {
fatalf("ChatStream: %v", err)
}
for ev := range ch {
switch ev.Kind {
case llm.EventText:
fmt.Print(ev.Text)
case llm.EventToolCall:
fmt.Printf("\n[tool call] %s(%s)\n", ev.ToolCall.Name, ev.ToolCall.Arguments)
case llm.EventUsage:
fmt.Printf("\n[usage] in=%d out=%d total=%d\n", ev.Usage.InputTokens, ev.Usage.OutputTokens, ev.Usage.TotalTokens)
case llm.EventDone:
fmt.Printf("[done] finish=%s\n", ev.Stop)
case llm.EventError:
fatalf("stream: %v", ev.Err)
}
}
flowchart LR
R["canonical Request"] --> C["ToChat"]
R --> RS["ToResponses"]
R --> A["ToAnthropic"]
R --> G["ToGemini"]
C --> W["各协议 wire body"]
RS --> W
A --> W
G --> W
W --> CV["Convert(from, to)
走 canonical 中转"]
J["DecideRequest"] --> JV["Jev / System One
自带 canonical 类型"]
classDef canon fill:#eef4fb,stroke:#2f5d8f,color:#22303a
classDef wire fill:#fdf6f0,stroke:#c8641f,color:#2b2722
class R,J canon
class C,RS,A,G,CV,JV wire
Convert 因此能两两互通。
Jev 有自己的 canonical 类型(DecideRequest /
DecideResponse),不进这个转换集。
Jev 决策问答
来自 main.go 的 Jev 段。Decide 接受一个
State(带类型的上下文)和一组命名问题,返回每个问题的答案与
把握度 Confidence。
dr, err := jc.Decide(context.Background(), llm.DecideRequest{
State: map[string]any{"message": "这个查询接口偶尔超时,帮我修一下。"},
Questions: map[string]llm.Question{
"intent": {Type: "choice", Instructions: "Which kind of request fits it best?",
Criteria: map[string]string{"greeting": "", "bug fix": "an ordinary fix", "feature": "a small feature"}},
"effort": {Type: "score", Instructions: "How much reasoning does a coding assistant need?",
Bands: []string{"a one-line change", "an ordinary fix", "a change across files"}},
},
})
验证
跑一遍,stdout 依次给出四段输出,末尾几行长这样:
$ go run ./examples/llm
== 一份 canonical 请求 → 四种协议 ==
-- OpenAI Chat Completions --
{ … "tool_calls": [ … ] … }
-- OpenAI Responses -- / -- Anthropic Messages -- / -- Google Gemini --
== Convert: Chat wire → Anthropic wire ==
== Chat(非流式)==
id=chatcmpl-1 model=demo-model finish=stop content="北京今天晴,21°C,适合散步。"
usage in=12 out=9 total=21
== ChatStream(流式)==
北京今天晴,21°C,适合散步。
[usage] in=12 out=9 total=21
[done] finish=stop
== Jev Decide(System One 决策问答)==
intent: bug fix (confidence 0.90)
effort: score 1
usage in=40 out=12 total=52
这些数字全部来自假上游的固定应答(见 newFakeChatServer /
newFakeJevServer),所以输出是确定的。WithBaseURL
指向的是 httptest 的本地地址,没有请求离开本机。
测试
main_test.go 复用同一对假上游(httptest),
完全离线。它钉住四件事:非流式 Chat 返回 id=chatcmpl-1、
finish=stop、content 含"散步"、usage 总量 21;
流式 Chat 收到 EventDone 且累积文本含"适合散步";
Jev Decide 的 choice 是 bug fix、score
是 1.0、usage 总量 52;四种协议的 wire body 各自含其关键字段
(tool_calls / function_call / tool_use /
functionCall),且 Convert 的 Chat→Anthropic 含
tool_use。
go test ./examples/llm
相关
- pkg/core 速查:
llm包的入口。 - 稳定性防护:
retry与WithRetry的退避语义。 - examples/resilience:限流、幂等、熔断、重试。
- examples/observability:把调用接进日志与链路。
- 示例索引。