cbd130ecae
dispatcher 不再手搓 pool.Stream,改用编译好的 Eino 图驱动;接入用户常驻画像,
推理前召回并注入 system prompt,实现个性化(架构'心脏'首次真跳)。
Eino 图(dispatcher/internal/eino): START→recall→prompt→model→END + 全局 State
- recall(Lambda): 取 Meta[user_id] → 调 MCP memory_get → ProcessState 写画像
- prompt(ChatTemplate): {profile} 注入 system,{query} 作 user
- model: poolModel 适配 LLM Pool 为 model.BaseChatModel(Generate+Stream, schema.Pipe)
- 写回: 流排空后异步 memorize(流式节点走 OnEndWithStreamOutput 非 OnEndFn)
记忆存储(mcp-go owns): GORM Profile→sundynix_user_profile(复合主键, AutoMigrate,
遵守前缀约定), 新工具 memory_get/memory_upsert, 连不上降级
Gateway: SubmitTask 注入 Meta[user_id](X-User-ID 头), PUT /api/v1/memory→memory_upsert
shared: contract.MetaUserID; llm.Pool 拆出 StreamText
验证: 4 模块 build✓ + 3 e2e PASS; live 跑通——PUT 偏好落 sundynix_user_profile,
带 X-User-ID 提交→Eino recall 召回→注入→SSE 流出含画像的个性化回答, writeback 触发
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
71 lines
2.2 KiB
Go
71 lines
2.2 KiB
Go
package eino
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
|
||
"github.com/cloudwego/eino/components/model"
|
||
"github.com/cloudwego/eino/schema"
|
||
|
||
"github.com/sundynix/sundynix-dispatcher/internal/llm"
|
||
)
|
||
|
||
// poolModel 把 LLM Pool 适配成 Eino 的 model.BaseChatModel,
|
||
// 让 LLM Pool 能作为图中的 ChatModel 节点参与编排与流式。
|
||
// 真实接入 vLLM/Ollama 后,这里替换为后端的 Generate/Stream 即可。
|
||
type poolModel struct{ pool *llm.Pool }
|
||
|
||
func newPoolModel(p *llm.Pool) *poolModel { return &poolModel{pool: p} }
|
||
|
||
var _ model.BaseChatModel = (*poolModel)(nil)
|
||
|
||
// Generate 阻塞式生成(图被 Invoke 时用)。
|
||
func (pm *poolModel) Generate(ctx context.Context, input []*schema.Message, _ ...model.Option) (*schema.Message, error) {
|
||
var sb strings.Builder
|
||
if err := pm.pool.StreamText(ctx, replyFor(input), func(tok []byte) { sb.Write(tok) }); err != nil {
|
||
return nil, err
|
||
}
|
||
return schema.AssistantMessage(sb.String(), nil), nil
|
||
}
|
||
|
||
// Stream 流式生成(图被 Stream 时用):把回复按 token 推进 pipe。
|
||
func (pm *poolModel) Stream(ctx context.Context, input []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) {
|
||
sr, sw := schema.Pipe[*schema.Message](32)
|
||
text := replyFor(input)
|
||
go func() {
|
||
defer sw.Close()
|
||
if err := pm.pool.StreamText(ctx, text, func(tok []byte) {
|
||
sw.Send(schema.AssistantMessage(string(tok), nil), nil)
|
||
}); err != nil {
|
||
sw.Send(nil, err)
|
||
}
|
||
}()
|
||
return sr, nil
|
||
}
|
||
|
||
// replyFor 是占位"模型":从消息中取出注入的画像与用户输入,
|
||
// 生成一段能体现"记忆已注入"的确定性回复(证明 recall→prompt 链路真的把画像喂进来了)。
|
||
// 真实模型不需要本函数。
|
||
func replyFor(msgs []*schema.Message) string {
|
||
var profile, user string
|
||
for _, m := range msgs {
|
||
switch m.Role {
|
||
case schema.System:
|
||
profile = m.Content
|
||
case schema.User:
|
||
user = m.Content
|
||
}
|
||
}
|
||
return "【已注入用户画像】" + condense(profile, 80) +
|
||
" | 据此为你个性化作答:已编排执行该 Agent 图(输入「" + condense(user, 30) + "」)。"
|
||
}
|
||
|
||
func condense(s string, max int) string {
|
||
s = strings.TrimSpace(strings.ReplaceAll(s, "\n", " "))
|
||
r := []rune(s)
|
||
if len(r) > max {
|
||
return string(r[:max]) + "…"
|
||
}
|
||
return s
|
||
}
|