Files
AskaEth 8890c30bb5
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(model): 按 provider 形状独立适配推理开关 + per-persona 提供商链真接线(裁决 03.4.2/08.4.1/03.4.1 余项)
- 新增 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 口径冲突,停手报告)
2026-10-04 12:57:55 +08:00

73 lines
3.3 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
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)
}