Files
sundynix-agentix/sundynix-dispatcher/internal/eino/compile.go
T
Blizzard 38a74e3911 fix(history): 空答复不落历史 + 发送前过滤空消息(根治会话毒化)
现象:某轮 LLM 返回空(余额不足/失败/降级)→ 空 assistant 消息被写进会话历史
→ 此后该 session 每次请求都带一条空 content 的 assistant 消息 → DeepSeek 400
"Invalid assistant message: content or tool_calls must be set" → 又空 → 又写空 → 自我循环毒化。

- Fix A(断源):orchestrator.memorize 对空答复直接 return,不落历史(失败的一轮不留痕)。
- Fix B(兜底):buildMessages 发送前剔除 content 与 tool_calls 均空的历史消息,
  防御任何来源的脏历史(含存量)。

验证:清理被污染的 default 会话后恢复正常;新逻辑下空答复不再写入。dispatcher
build+vet+test 全绿。

注:诊断中发现激活模型 deepseek-v4-pro 是推理模型(答案在 reasoning_content,content 为空),
已在管理端切回 deepseek-chat。仍待办:LLM 调用失败应暴露为 failed 而非 done-空输出。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:53:30 +08:00

74 lines
2.9 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"
"encoding/json"
"strings"
"github.com/cloudwego/eino/components/prompt"
"github.com/cloudwego/eino/schema"
)
// RunCtx 是组装模型消息用的上下文:图解释器(graph.go)把黑板汇总到它,
// buildMessages 据此拼出发给模型的消息序列。用统一结构避免散落多处拼装。
type RunCtx struct {
UserID string
SessionID string
System string // Agent 节点系统提示词
Query string // 用户输入
Profile string // 召回的画像
History []*schema.Message // 短期历史
ToolOut []string // 工具/检索节点产出(含参考资料)
Upstream []string // 前序协作 agent 的产出(多 agent 接力时注入,让下游基于上游继续)
}
// chatTemplate 是会话消息模板:系统提示词 + 历史占位 + 用户输入。
// 用 Eino components/prompt.ChatTemplateFString 仅解析模板串、值原样注入,故 JSON 花括号安全)。
var chatTemplate = prompt.FromMessages(schema.FString,
schema.SystemMessage("{system}"),
schema.MessagesPlaceholder("history", true),
schema.UserMessage("{query}"),
)
// buildMessages 把上下文组装为发给模型的消息序列(系统提示词 + 画像 + 工具产出 + 历史 + 用户输入)。
// 系统串的动态拼装(按需注入画像/参考资料)留在 Go 侧;最终经 ChatTemplate 成型。
func buildMessages(ctx context.Context, rc *RunCtx) ([]*schema.Message, error) {
var sys strings.Builder
sys.WriteString(rc.System)
if rc.Profile != "" {
sys.WriteString("\n\n关于当前用户的已知信息:\n")
sys.WriteString(rc.Profile)
sys.WriteString("\n请据此个性化作答并保持其偏好。")
}
if len(rc.ToolOut) > 0 {
sys.WriteString("\n\n以下是工具/检索得到的参考资料:\n")
sys.WriteString(strings.Join(rc.ToolOut, "\n---\n"))
}
if len(rc.Upstream) > 0 {
sys.WriteString("\n\n以下是前序协作 agent 的产出,请在此基础上继续完成你的部分(不要重头再来):\n")
sys.WriteString(strings.Join(rc.Upstream, "\n---\n"))
}
// 防御:剔除空 content 的历史消息。OpenAI 兼容 API 拒绝「content 与 tool_calls 都为空」的
// assistant 消息(400 Invalid assistant message),一条脏历史会毒化整段会话的后续请求。
history := make([]*schema.Message, 0, len(rc.History))
for _, m := range rc.History {
if m == nil || (strings.TrimSpace(m.Content) == "" && len(m.ToolCalls) == 0) {
continue
}
history = append(history, m)
}
return chatTemplate.Format(ctx, map[string]any{
"system": sys.String(),
"history": history,
"query": rc.Query,
})
}
// previewArgs 把工具入参压成一行短预览。
func previewArgs(args map[string]any) string {
if data, err := json.Marshal(args); err == nil {
return truncate(string(data), 120)
}
return ""
}