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

它演示了什么

四种协议的渲染

来自 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
四种对话协议共享 canonical 中转,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

相关