- 新增 reasoning_shape.go:**每个 provider 形状一个 resolver,只发自己形状的字段**
· deepseek:thinking{type} + reasoning_effort(依据 DevDocs/reference/deepseek 官方本地副本)
· vllm:chat_template_kwargs.enable_thinking · dashscope:顶层 enable_thinking(**只允许显式声明**)
· openai:仅 reasoning_effort · anthropic:自己的 thinking{budget_tokens}(未改)
· 未知 kind → WARN 且一个字段都不发(不许静默失效)
- 形状载体:models.json 组级 `reasoning.kind`(可热更新,未声明则不写该键)+ env `LLM_REASONING_KIND`
- 红线:`effort:none` 在只有布尔开关的形状上翻成**显式关思考**,绝不降成任何强度档
- 03.4.1 余项:`Request.PersonaID` + `RuntimeLLM.ForPersona(pid)` 视图;
每 persona 协议一条链并消费 `RouteProviderForPersona`;`agent/request.go` 非流式路径同一钩子
- **生产装配(主会话补)**:默认 agent 与 personaAgentPool 都改用 `ForPersona(pid)` 视图——
否则角色级模型/提供商路由在生产里永不被选中(形同虚设)
- 复核:build/vet 绿 + model/agent/config/cmd 包测试绿;5 条变异反证先红后还原
(其中一条「红但没钉住」已改用例:通道读取加 2s 超时,避免整包 panic 掩盖信号)
- 未落:DashScope 的 auto 推断(缺官方依据,只允许显式声明);Qwen `/think` 前导消息
(design/01 §2.1 与 design/11 §3 口径冲突,停手报告)
73 lines
3.3 KiB
Go
73 lines
3.3 KiB
Go
package model
|
||
|
||
import "context"
|
||
|
||
// 本文件实现「固定角色」的 LLM 栈视图(裁决 03.4.1 的 per-persona provider:
|
||
// 跨协议换 adapter)。
|
||
//
|
||
// 为什么需要它:跨协议的 per-persona 路由要求「请求携带 persona」才能选链,
|
||
// 而 persona 只存在于 loop 的会话环境里(挂在 loop 自己的私有 ctx 键上),
|
||
// 且 model 不能反向 import loop。但 loop 取适配器的位置恰好是**按角色**的:
|
||
// `loop.streamStep` 用 `ag.Adapter()`,而 agent 池是一角色一个 agent
|
||
// (design/22 §4.2 方案 A)。于是「这个 agent 服务哪个角色」由装配处一次性
|
||
// 决定、由适配器视图携带,是唯一不外泄、可测、且不动 loop/agent 结构的载体。
|
||
//
|
||
// 视图只做两件事:把角色贴到每个请求上(PersonaID),并按该角色路由模型名;
|
||
// 选链与降级仍由 RuntimeLLM 完成(它看不到 loop 的角色解析结果)。
|
||
|
||
// PersonaScoped 由「固定服务某个角色」的适配器视图实现。agent 的非流式路径
|
||
// (internal/runtime/agent/request.go)读它,好让同一个 persona 钩子在
|
||
// RunStep 与流式主路径上都成立。空字符串表示该视图不绑定角色。
|
||
type PersonaScoped interface {
|
||
// ScopedPersona 返回该视图固定的角色 id。
|
||
ScopedPersona() string
|
||
}
|
||
|
||
// PersonaStack 是 LLM 栈的角色视图:它实现 Adapter(贴角色后委托给底层栈)
|
||
// 与 ModelRouter(按角色路由模型名)。把它当作某一角色的 agent 的
|
||
// adapter 与 router 注入,该 agent 的每个请求就都带上了这个角色。
|
||
//
|
||
// 它是无状态的,可安全并发使用。
|
||
type PersonaStack struct {
|
||
persona string
|
||
inner *RuntimeLLM
|
||
}
|
||
|
||
// ForPersona 返回服务 persona 的栈视图。空白 persona 返回一个不绑定角色的
|
||
// 视图(等价于底层栈),使「没解析出角色」的装配处保持既有行为。
|
||
func (m *RuntimeLLM) ForPersona(persona string) *PersonaStack {
|
||
return &PersonaStack{persona: trimKey(persona), inner: m}
|
||
}
|
||
|
||
// ScopedPersona 实现 PersonaScoped。
|
||
func (p *PersonaStack) ScopedPersona() string { return p.persona }
|
||
|
||
// Name 实现 Adapter。带上角色后缀,使 CallRecord 与日志能分辨是哪个角色在
|
||
// 说话;未绑定角色时与底层栈同名。
|
||
func (p *PersonaStack) Name() string {
|
||
if p.persona == "" {
|
||
return p.inner.Name()
|
||
}
|
||
return p.inner.Name() + ":" + p.persona
|
||
}
|
||
|
||
// Chat 实现 Adapter:贴上角色后委托给底层栈。
|
||
func (p *PersonaStack) Chat(ctx context.Context, req Request, opts Options) (Response, error) {
|
||
req.PersonaID = p.persona
|
||
return p.inner.Chat(ctx, req, opts)
|
||
}
|
||
|
||
// ChatStream 实现 Adapter:贴上角色后委托给底层栈。
|
||
func (p *PersonaStack) ChatStream(ctx context.Context, req Request, onEvent func(StreamEvent)) (Response, error) {
|
||
req.PersonaID = p.persona
|
||
return p.inner.ChatStream(ctx, req, onEvent)
|
||
}
|
||
|
||
// RouteRequest 实现 ModelRouter:按该视图的角色路由模型名,未绑定角色时
|
||
// RouteRequestForPersona 会回落到消息 role 的同一条老路。它刻意不实现
|
||
// PersonaRouter:loop 在拿到真实角色时自己调 RouteRequestForPersona,
|
||
// 视图再顶一层只会让「谁的角色说了算」含糊。
|
||
func (p *PersonaStack) RouteRequest(req Request) (string, bool) {
|
||
return p.inner.RouteRequestForPersona(p.persona, req)
|
||
}
|