Files
sundynix-agentix/sundynix-dispatcher/internal/eino/model.go
T
Blizzard cbd130ecae feat: 第一张真实 Eino 图 + 偏好记忆(让模型知道是我)
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>
2026-06-10 14:06:18 +08:00

71 lines
2.2 KiB
Go
Raw 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 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
}