157 lines
3.8 KiB
Go
157 lines
3.8 KiB
Go
package eino
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
|
||
"github.com/cloudwego/eino-ext/components/model/qwen"
|
||
"github.com/cloudwego/eino/components/prompt"
|
||
"github.com/cloudwego/eino/schema"
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
"github.com/gogf/gf/v2/os/glog"
|
||
"github.com/gogf/gf/v2/util/gconv"
|
||
)
|
||
|
||
const (
|
||
MaxHistoryTurns = 5 // 最大历史轮数
|
||
)
|
||
|
||
var (
|
||
globalChatModel *qwen.ChatModel
|
||
ragPromptTemplate prompt.ChatTemplate // EINO 官方模板
|
||
)
|
||
|
||
func init() {
|
||
ctx := context.Background()
|
||
// 初始化大模型
|
||
if err := initChatModel(ctx); err != nil {
|
||
glog.Errorf(ctx, "初始化大模型失败: %v", err)
|
||
}
|
||
// 初始化 EINO 提示词模板
|
||
initRAGPromptTemplate()
|
||
|
||
return
|
||
}
|
||
|
||
// 初始化通义千问
|
||
func initChatModel(ctx context.Context) error {
|
||
if globalChatModel != nil {
|
||
return nil
|
||
}
|
||
|
||
apiKey := g.Cfg().MustGet(ctx, "eino.chatmodel.apiKey").String()
|
||
model := g.Cfg().MustGet(ctx, "eino.chatmodel.model").String()
|
||
|
||
cm, err := qwen.NewChatModel(ctx, &qwen.ChatModelConfig{
|
||
APIKey: apiKey,
|
||
Model: model,
|
||
BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||
Timeout: 60 * 1e9,
|
||
Temperature: gconv.PtrFloat32(0.7), // 客服最佳
|
||
MaxTokens: gconv.PtrInt(1024), // 最长回答
|
||
TopP: gconv.PtrFloat32(1.0),
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
globalChatModel = cm
|
||
return nil
|
||
}
|
||
|
||
// 初始化 EINO 官方提示词模板(最关键!)
|
||
func initRAGPromptTemplate() {
|
||
ragPromptTemplate = prompt.FromMessages(
|
||
schema.FString,
|
||
// 系统提示(带参考知识)
|
||
&schema.Message{
|
||
Role: schema.System,
|
||
Content: `你是专业客服,语气友好简洁。
|
||
请严格依据参考知识回答,不知道就说:抱歉,我暂时无法回答这个问题。
|
||
|
||
参考知识:
|
||
{knowledge}`,
|
||
},
|
||
// 用户问题
|
||
&schema.Message{
|
||
Role: schema.User,
|
||
Content: "{question}",
|
||
},
|
||
)
|
||
}
|
||
|
||
// NewChatModel 只处理逻辑,不复用创建模型
|
||
func NewChatModel(ctx context.Context, question string, docs []*schema.Document, history []*schema.Message) (replyMsg *schema.Message, err error) {
|
||
// 1. 构建参考知识
|
||
knowledge := buildKnowledgeAndSources(docs)
|
||
// 2. 历史精简
|
||
history = limitHistory(history)
|
||
// 3. ✅ EINO 官方模板格式化(超级干净)
|
||
msgs, err := ragPromptTemplate.Format(ctx, map[string]any{
|
||
"knowledge": knowledge,
|
||
"question": question,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
// 4. 历史插入到模板消息中间(标准EINO用法)
|
||
if len(history) > 0 {
|
||
msgs = append(msgs[:1], append(history, msgs[1:]...)...)
|
||
}
|
||
// 5. 🔥 直接使用全局单例,不重复创建
|
||
replyMsg, err = streamGenerateAnswer(ctx, globalChatModel, msgs)
|
||
|
||
return
|
||
}
|
||
|
||
func limitHistory(history []*schema.Message) []*schema.Message {
|
||
valid := make([]*schema.Message, 0, len(history))
|
||
for _, m := range history {
|
||
if m.Role == schema.User || m.Role == schema.Assistant {
|
||
valid = append(valid, m)
|
||
}
|
||
}
|
||
|
||
keep := 2 * MaxHistoryTurns
|
||
if len(valid) > keep {
|
||
valid = valid[len(valid)-keep:]
|
||
}
|
||
return valid
|
||
}
|
||
|
||
// buildKnowledgeAndSources 拼接参考知识
|
||
func buildKnowledgeAndSources(docs []*schema.Document) string {
|
||
var knowledge string
|
||
|
||
for i, doc := range docs {
|
||
knowledge += fmt.Sprintf("[参考%d] %s\n", i+1, doc.Content)
|
||
|
||
}
|
||
return knowledge
|
||
}
|
||
|
||
// streamGenerateAnswer 流式生成
|
||
func streamGenerateAnswer(ctx context.Context, chatModel *qwen.ChatModel, msgs []*schema.Message) (reply *schema.Message, err error) {
|
||
|
||
sr, err := chatModel.Stream(ctx, msgs)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("stream failed: %w", err)
|
||
}
|
||
|
||
var chunks []*schema.Message
|
||
for {
|
||
chunk, err := sr.Recv()
|
||
if errors.Is(err, io.EOF) {
|
||
break
|
||
}
|
||
if err != nil {
|
||
return nil, fmt.Errorf("stream recv failed: %w", err)
|
||
}
|
||
chunks = append(chunks, chunk)
|
||
}
|
||
|
||
return schema.ConcatMessages(chunks)
|
||
}
|