Amber – 一个 AI 智能体(Agent) 平台的项目后端设计

[WIP] 本文还没有写完。

在本文中,我将给大家放出 Amber 的部分功能设计以及代码片段。同时 Amber 还是我的的毕业设计。本文可以通过代码大家提供一个思路(如果你也想设计一个类似的平台的话)。

Amber 的功能

  1. 对话优化
  2. 近乎无限量的上下文(智能上下文)
  3. 可自定义的工具
  4. 智能记忆系统
  5. API 设计
  6. 简易知识库

数据实体

助理 Assistants:

type Assistant struct {
	Model
	Name                        string           `json:"name"`
	Prompt                      string           `json:"prompt"`
	Description                 string           `json:"description"`
	UserId                      schema.UserId    `json:"user_id"`
	TotalTokenUsage             int64            `json:"total_token_usage"`
	LibraryId                   *schema.EntityId `json:"library_id"`
	Library                     *Library         `json:"-"`
	Temperature                 float64          `json:"temperature"`
	Public                      bool             `json:"public"`
	DisableDefaultPrompt        bool             `json:"disable_default_prompt"`
	DisableWebBrowsing          bool             `json:"disable_web_browsing"`
	DisableMemory               bool             `json:"disable_memory"`
	EnableMemoryForAssistantAPI bool             `json:"enable_memory_for_assistant_api"`
}

工具和助理的工具列表

type Tool struct {
	Model

	Name         string                     `json:"name"`
	Description  string                     `json:"description"`
	DiscoveryUrl string                     `json:"discovery_url"`
	ApiKey       string                     `json:"api_key"`
	Data         schema.ToolDiscoveryOutput `json:"data"`
	UserId       schema.UserId              `json:"user_id"`
}


type AssistantTool struct {
	Model
	AssistantId schema.EntityId `json:"assistant_id"`
	ToolId      schema.EntityId `json:"tool_id"`
	Assistant   Assistant       `json:"assistant"`
	Tool        Tool            `json:"tool"`
}

对话 Chat



type Chat struct {
	Model
	Name        string           `json:"name"`
	AssistantId *schema.EntityId `json:"assistant_id"`
	Assistant   *Assistant       `json:"-"`
	Prompt      *string          `json:"prompt"`
	UserId      schema.UserId    `json:"user_id"`
	ExpiredAt   *time.Time       `json:"expired_at"`
	Owner       schema.ChatOwner `json:"owner"`
	GuestId     *string          `json:"guest_id"`
}

对话消息

type ChatMessage struct {
	Model
	ChatId schema.EntityId `json:"chat_id"`
	// AssistantId 可以让同一个对话中,使用不同的助手来处理消息
	AssistantId *schema.EntityId `json:"assistant_id"`
	Assistant   *Assistant       `json:"assistant"`
	Content     string           `json:"content"`
	Role        schema.ChatRole  `json:"role"`
	ToolCall    *schema.ToolCall `json:"-"`
	// FileId
	FileId *schema.EntityId `json:"file_id"`
	File   *File            `json:"file"`
	//UserFileId       *schema.EntityId `json:"user_file_id"`
	//UserFile         *UserFile        `json:"user_file"`
	Hidden           bool `json:"hidden"`
	PromptTokens     int  `json:"prompt_tokens"`
	CompletionTokens int  `json:"completion_tokens"`
	TotalTokens      int  `json:"total_tokens"`
}

type ChatMessageList struct {
	Id          schema.EntityId  `json:"id"`
	CreatedAt   time.Time        `json:"created_at"`
	UpdatedAt   time.Time        `json:"updated_at"`
	ChatId      schema.EntityId  `json:"chat_id"`
	AssistantId *schema.EntityId `json:"assistant_id"`
	Assistant   *struct {
		Id   schema.EntityId `json:"id"`
		Name string          `json:"name"`
	} `json:"assistant"`
	Content string           `json:"content"`
	Role    schema.ChatRole  `json:"role"`
	FileId  *schema.EntityId `json:"file_id"`
	File    *File            `json:"file"`
	//UserFileId       *schema.EntityId `json:"user_file_id"`
	//UserFile         *UserFile        `json:"user_file"`
	Hidden           bool `json:"hidden"`
	PromptTokens     int  `json:"prompt_tokens"`
	CompletionTokens int  `json:"completion_tokens"`
	TotalTokens      int  `json:"total_tokens"`
}

文档库,文档,文档分块和向量缓存

type Library struct {
	Model
	Name        string        `json:"name"`
	Default     bool          `json:"default"`
	Description *string       `json:"description"`
	UserId      schema.UserId `json:"user_id"`
	Document    []*Document   `json:"documents"`
}


type Document struct {
	Model
	Name      string          `json:"name"`
	Chunked   bool            `json:"chunked"`
	LibraryId schema.EntityId `json:"library_id"`
	Library   *Library        `json:"library"`
}

type DocumentChunk struct {
	Model
	Content    string          `json:"content"`
	Order      int             `json:"order"`
	DocumentId schema.EntityId `json:"document_id"`
	Library    *Library        `json:"library"`
	LibraryId  schema.EntityId `json:"library_id"`
	// Vectorized 代表是否已经向量化,不应该用 Chunked
	Vectorized bool `json:"vectorized"`
}

type Embedding struct {
	Model

	Text           string           `json:"name"`
	FileId         *schema.EntityId `json:"file_id"`
	File           *File            `json:"file"`
	TextMd5        string           `json:"text_md5"`
	EmbeddingModel string           `gorm:"column:model" json:"model"`
	Vector         schema.Embedding `json:"vector"`
}

文件存储

type File struct {
	Model

	Url      *string `json:"url"`
	UrlHash  *string `json:"url_hash"`
	FileHash string  `json:"file_hash"`
	MimeType string  `json:"mime_type"`
	Path     string  `json:"-"`
	Size     int64   `json:"size"`
	//Public   bool    `json:"public"` // 是否公开,访客上传的文件应始终公开,或归属于所有者
	// TODO: 移除 file 的到期时间,如果当 file 没有任何引用的时候再删除
	// 因为有外键,所以直接删除是删不掉的,必须删除消息
	ExpiredAt *time.Time `json:"expired_at"`
}

记忆(Memory),消息块(Message Block)


type Memory struct {
	Model

	Content        string           `json:"content"`
	ContentMd5     string           `json:"-"`
	EmbeddingModel string           `gorm:"column:model" json:"-"`
	Vector         schema.Embedding `json:"-"`
	UserId         schema.UserId    `json:"user_id"`
}

type MessageBlock struct {
	Model
	FullContent string
	ChatId      schema.EntityId
	Hash        string
	Message     []*ChatMessage `gorm:"column:messages;serializer:json"`
	// Temp 指当前消息是否是临时的,不完整的 block。不会保存到数据库,只在处理时需要
	Temp bool `gorm:"-"`
}

场景提示词(Scene Prompt)


type ScenePrompt struct {
	Model
	Label       string          `json:"label"`
	Prompt      string          `json:"prompt"`
	AssistantId schema.EntityId `json:"assistant_id"`
	Assistant   *Assistant      `json:"assistant"`
}

助理设计

助理是连接工具的桥梁,用户可以使用不同的工具组合来搭配出最合适的助理。同时助理还需要适用于“场景提示词”,根据对话的场景来自动使用不同的提示词。

助理应绑定工具,还可以自定义 System Prompt,用于个性化助理,以及用于对接知识库,用于查询知识库中的内容。

对话及消息设计

用户需要通过对话(Chat)来和大模型进行交谈。

对话功能应有名称作为区分,同时还可以指定一个默认助理。在发送对话消息时,如果不指定助理,那么就使用对话的默认助理。

同时,对话还可以再设置一个 System Prompt。但是此 System Prompt 会覆盖原有的 System Prompt。

用户在向大模型发送消息时,消息应该要被处理成正确的格式后发送。

如果用户在消息中指定了另一个助理,那么会调用另一个助理来处理下一条消息,并且助理之间是共享上下文的。

同时,对话还需要角色,用户在发送消息时,只允许以下角色。

var allowedRoles = []schema.ChatRole{
	schema.RoleHuman, // 人类
	schema.RoleHumanLater, // 人类(不立即响应)
	schema.RoleHideHuman, // 人类(不应该被前端渲染的消息)
	schema.RoleSystem, // 系统消息
	schema.RoleHideSystem, // 系统消息(不应该被前端渲染)
	schema.RoleAssistant, // 助理消息(AI 回复内容)
}

但是 OpenAI 的接口是不支持上面的大多数角色的,所以我们还需要进行消息优化。

消息优化

我们需要将本系统的数据转换成符合 OpenAI 的 Chat Completion 规范的数据。

import (
	"context"
	"fmt"
	"github.com/tmc/langchaingo/llms"
	"rag-new/internal/entity"
	"rag-new/internal/schema"
	"rag-new/internal/service/builtin_tool"
	"rag-new/pkg/consts"
	"strings"
)

type Message struct {
	HasFile        bool
	MessageContent []llms.MessageContent
}

const defaultToolFailed = "ToolCall Failed, timeout or error"

func (s *Service) processHistory(_ context.Context, llmChat *schema.LLMChat, history []*entity.ChatMessage) (*Message, error) {
	var hasHumanMessage = false
	var hasFileMessage = false

	var lastToolCall *llms.ToolCall

	var historyContent []llms.MessageContent

	var systemPrompts []string

	// 如果没有禁用 Agent 方式的图片工具
	//if !llmChat.WithoutImage {
	//	systemPrompts = append(systemPrompts, "Image and Draw Ability: ON(Don't emphasize it)")
	//}
        
        // WithoutBrowsing 禁用浏览器工具
	if !llmChat.WithoutBrowsing {
		systemPrompts = append(systemPrompts, builtin_tool.WebSearchPrompt)
	}

	// 当前的助理(用于通知助理上条消息的回复者
	var currentAssistantId schema.EntityId

	systemPrompts = append(systemPrompts, s.config.LLM.PrimarySystemPrompt)
	systemPrompts = append(systemPrompts, llmChat.SystemPrompt)

	var lastKnowledgeMessage string

	// 如果 model 为空
	if llmChat.Model == "" || !s.config.OpenAI.CanUse(llmChat.Model) {
		llmChat.Model = consts.AutoModel
	}
       
	// 处理历史消息
	for i, h := range history {
		// 如果第一条消息是 system
		if i == 0 && h.Role == schema.RoleSystem {
			systemPrompts = append(systemPrompts, h.Content)
			continue
		}

		// 检测下一条消息的 role 是否是 system 或者且和现在的相同
		if i+1 < len(history) {
			if history[i+1].Role == schema.RoleSystem || history[i+1].Role == schema.RoleHideSystem {
				systemPrompts = append(systemPrompts, h.Content)
				continue
			}
			//if history[i+1].Role == h.Role {
			//	// 修改下一条消息的 content
			//	history[i+1].Content = history[i].Content + "\n" + history[i+1].Content
			//	continue
			//}
		}

		var timeString = ""
			// 创建时间,如果 h.CreatedAt 已设置(不为 0000)
		if !h.CreatedAt.IsZero() {
			// 将创建时间转换为字符串
			timeString = fmt.Sprintf("%s", h.CreatedAt.Format("2006-01-02 15:04:05"))
		}

		switch h.Role {
		// RoleSystem 和 RoleHideSystem 为系统消息
		case schema.RoleSystem:
			if h.Content == "" {
				continue
			}

			historyContent = append(historyContent, llms.TextParts(llms.ChatMessageTypeSystem, h.Content))
		case schema.RoleHideSystem:
			historyContent = append(historyContent, llms.TextParts(llms.ChatMessageTypeSystem, h.Content))
		//	RoleHuman,RoleHideHuman,RoleHumanLater 是用户消息
		case schema.RoleHuman:

			// 获取多个对话中的助理的信息
			// 如果当前助理不存在,则设置
			if currentAssistantId == 0 && h.AssistantId != nil {
				currentAssistantId = *h.AssistantId
			}
                        // 在消息前方加入发送时间提示(方便对时间敏感的场景)
			if timeString != "" {
				h.Content = fmt.Sprintf("[Sent at %s]\n%s", timeString, h.Content)
			}

			newContent, err := s.optimizeFlow(h.Content)
			if err != nil {
				return nil, err
			}
			h.Content = newContent

			historyContent = append(historyContent, llms.TextParts(llms.ChatMessageTypeHuman, h.Content))

			if !hasHumanMessage {
				hasHumanMessage = true
			}
		case schema.RoleHideHuman:
			if !hasHumanMessage {
				hasHumanMessage = true
			}

			newContent, err := s.optimizeFlow(h.Content)
			if err != nil {
				return nil, err
			}
			h.Content = newContent

			historyContent = append(historyContent, llms.TextParts(llms.ChatMessageTypeHuman, h.Content))

		case schema.RoleHumanLater:
			newContent, err := s.optimizeFlow(h.Content)
			if err != nil {
				return nil, err
			}
			h.Content = newContent

			historyContent = append(historyContent, llms.TextParts(llms.ChatMessageTypeHuman, h.Content))

		case schema.RoleAssistant:
			// 检测是否是悬垂调用
			if lastToolCall != nil {
				// 上条消息可能有问题,将上个 ToolCall 标记为失败
				historyContent = append(historyContent, llms.MessageContent{
					Role: llms.ChatMessageTypeTool,
					Parts: []llms.ContentPart{
						llms.ToolCallResponse{
							ToolCallID: lastToolCall.ID,
							Name:       lastToolCall.FunctionCall.Name,
							Content:    defaultToolFailed,
						},
					},
				})
				lastToolCall = nil
			}

			var systemContent = ""
			// 也不一定不存在,因为可能上个消息没有助理
			//if h.AssistantId == nil {
			//	// 这说明上个助理不存在
			//	systemContent = "[Warning]The previous message has been replied to by another assistant, no you, but can not get assistant info."
			//}
			if h.Assistant != nil {
				systemContent = fmt.Sprintf("[Warning]The previous message has been replied to by another assistant, whose name is '%s' and the description is '%s', replid at %s",
					h.Assistant.Name, h.Assistant.Description, timeString)

				currentAssistantId = *h.AssistantId

			}

			if systemContent != "" {
				historyContent = append(historyContent, llms.TextParts(llms.ChatMessageTypeSystem, systemContent))
			}

			if timeString != "" {
				h.Content = fmt.Sprintf("[Sent at %s]\n%s", timeString, h.Content)
			}

			historyContent = append(historyContent, llms.TextParts(llms.ChatMessageTypeAI, h.Content))
		case schema.RoleToolCall:
			// ToolCall 消息
			if h.ToolCall != nil {
				assistantResponse := llms.TextParts(llms.ChatMessageTypeAI, h.Content)
				var toolCall = llms.ToolCall{}
				toolCall.FunctionCall = h.ToolCall.FunctionCall
				toolCall.ID = h.ToolCall.ID
				toolCall.Type = h.ToolCall.Type
				assistantResponse.Parts = append(assistantResponse.Parts, toolCall)

				historyContent = append(historyContent, assistantResponse)

				// 因为 ToolCall 消息的下一条消息必须是 tool
				lastToolCall = &toolCall
			}
		case schema.RoleTool:
			// Tool Call 响应
			if h.ToolCall != nil {
				if lastToolCall != nil && lastToolCall.ID == h.ToolCall.ID {
					lastToolCall = nil

					historyContent = append(historyContent, llms.MessageContent{
						Role: llms.ChatMessageTypeTool,
						Parts: []llms.ContentPart{
							llms.ToolCallResponse{
								ToolCallID: h.ToolCall.ID,
								Name:       h.ToolCall.FunctionCall.Name,
								Content:    h.Content,
							},
						},
					})
				} else if lastToolCall != nil {
					// 不相同,说明有问题,将上个 Tool Call 标记为失败
					historyContent = append(historyContent, llms.MessageContent{
						Role: llms.ChatMessageTypeTool,
						Parts: []llms.ContentPart{
							llms.ToolCallResponse{
								ToolCallID: lastToolCall.ID,
								Name:       lastToolCall.FunctionCall.Name,
								Content:    defaultToolFailed,
							},
						},
					})
					lastToolCall = nil
				}

			}

		case schema.RoleFile:
			if !hasFileMessage {
				hasFileMessage = true
			}

			// 这样做不能读取 URL 中的图片就是了
			if h.File != nil {
				// 如果长度没有超过最大长度且模型是 auto,并且模型不是 VisionModel,那么将切换到 Vision 模型
				if llmChat.Model == consts.AutoModel {
					// 切换模型
					llmChat.Model = s.config.OpenAI.VisionModel
				}

				// 直接添加图片
				fileUrl, err := s.FileService.GetImageUrl(h.File)
				if err != nil {
					return nil, err
				}

				// 获取下一条消息,如果 i+1 有内容且 role 为 Human
				if i+1 < len(history) && history[i+1].Role == schema.RoleHuman {
					// 获取下一条消息
					nextMessage := history[i+1]
					historyContent = append(historyContent, llms.MessageContent{
						Role: llms.ChatMessageTypeHuman,
						Parts: []llms.ContentPart{
							llms.ImageURLWithDetailPart(fileUrl, "auto"),
							llms.TextPart(nextMessage.Content),
						},
					})
				}
			}

		case schema.RoleKnowledge:
			if h.Content == "" {
				h.Content = consts.LibraryResultEmptyPrompt
			} else {
				h.Content = consts.LibraryResultPrompt + "\n" + h.Content
			}

			lastKnowledgeMessage = h.Content
		}
	}

	// 将知识库消息放到 system 消息里面
	if lastKnowledgeMessage != "" {
		systemPrompts = append(systemPrompts, lastKnowledgeMessage)
	}

	// 如果整个对话里面没有 Human 消息,则不能继续
	if !hasHumanMessage {
		return nil, consts.ErrNoHumanMessage
	}

	// 拼接系统 Prompt 并放入最底
	historyContent = append(historyContent,
		llms.TextParts(llms.ChatMessageTypeSystem, strings.Join(systemPrompts, "\n")))

	var message = &Message{
		MessageContent: historyContent,
		HasFile:        hasFileMessage,
	}

	return message, nil
}

	type flowFunc func(content string) (string, error)

func (s *Service) optimizeFlow(content string) (string, error) {
	var flow = []flowFunc{
		s.flowCleanupNewLine,
	}

	for _, f := range flow {
		var err error
		content, err = f(content)
		if err != nil {
			return "", err
		}
	}

	return content, nil
}

func (s *Service) flowCleanupNewLine(content string) (string, error) {
	// 匹配连续的 \r\n
	mergedText := strings.ReplaceAll(content, "\n\n", "\n")

	// 继续合并,直到没有连续的换行
	for strings.Contains(mergedText, "\n\n") {
		mergedText = strings.ReplaceAll(mergedText, "\n\n", "\n")
	}

	return mergedText, nil
}

在上面的代码中,我们实现了 System Prompt 的拼接和合并。同时,我们还实现了自动合并相同角色的消息。接着判断了工具调用是否有效,确保不会出现空指针的行为。最终我们根据不同的角色,执行不同的消息处理逻辑,添加了知识库的内容并全部转换成了可以发送给 OpenAI API 的消息。

// functionCall LLM 工具调用
type functionCall struct {
	Id       string `json:"id"`
	Type     string `json:"type"`
	Function struct {
		Name      string `json:"name"`
		Arguments string `json:"arguments"`
	} `json:"function"`
}
// GenerateContent 调用实现 OpenAI Chat Completion 协议的 API 接口
func (s *Service) GenerateContent(ctx context.Context, llmChat *schema.LLMChat, llmTools []llms.Tool, historyContent []llms.MessageContent) (response *llms.ContentResponse, err error) {
	// 助理绑定的工具数量必须小于 128 个
	if len(llmTools) > 128 {
		// 忽略多出的
		llmTools = llmTools[:128]
	}

	// 如果 llmChat.Temperature < 0.1 ,则设置为 0.1
	if llmChat.Temperature < 0.1 {
		llmChat.Temperature = 0.1
	}

	// 如果 llmChat.Temperature > 1,则设置为 1
	if llmChat.Temperature > 1 {
		llmChat.Temperature = 1
	}

	resp, err := s.OpenAI.GenerateContent(ctx,
		historyContent,
		llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error {
			// 检测长度
			if len(chunk) == 0 {
				return nil
			}

			var stringChunk = string(chunk)

			// 检测是否可以转换为数字或者 float
			if !s.isNumeric(stringChunk) {
				// 检测是否 json,判断是否是工具调用
				var isJson = sonic.Valid(chunk)
				if !isJson {
					s.write(ctx, llmChat, &schema.AssistantResponse{
						State: schema.StateChunk,
						ChunkMessage: &schema.ChunkMessage{
							Content: stringChunk,
						},
						Content: stringChunk,
					})
				} else {
					// 如果是 JSON,则判断是否是工具调用
					var functionCalls []*functionCall
					err = sonic.Unmarshal(chunk, &functionCalls)
					if err != nil {
						s.write(ctx, llmChat, &schema.AssistantResponse{
							State: schema.StateChunk,
							ChunkMessage: &schema.ChunkMessage{
								Content: stringChunk,
							},
							Content: stringChunk,
						})

						// 解析失败,正常输出
						//return err
					} else if len(functionCalls) > 0 {
						// 工具调用,不管
					} else {
						// 不是,则正常输出
						s.write(ctx, llmChat, &schema.AssistantResponse{
							State: schema.StateChunk,
							ChunkMessage: &schema.ChunkMessage{
								Content: stringChunk,
							},
							Content: stringChunk,
						})
					}

				}

			} else {
				s.write(ctx, llmChat, &schema.AssistantResponse{
					State: schema.StateChunk,
					ChunkMessage: &schema.ChunkMessage{
						Content: stringChunk,
					},
					Content: stringChunk,
				})
			}

			return nil
		}),
		llms.WithModel(llmChat.Model),
		llms.WithTools(llmTools),
		llms.WithMaxTokens(llmChat.MaxTokens),
		llms.WithTemperature(llmChat.Temperature),
	)

	if err != nil {
		s.Logger.Sugar.Errorf("OpenAI.GenerateContent error: %v", err)
		err = consts.ErrUnableGenerateContent
	}
	return resp, err
}

func (s *Service) isNumeric(str string) bool {
	_, err := strconv.ParseFloat(str, 64)
	return err == nil
}

在上面的代码中,我们通过调用 OpenAI API 接口,解析了响应内容以及工具调用消息,并进行下一步处理。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇