部署DeepSeek模型,进群交流最in玩法!
立即加群
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >字节跳动Golang大模型应用开发框架Eino AI Agent开发实例-Infortress自然语言搜索

字节跳动Golang大模型应用开发框架Eino AI Agent开发实例-Infortress自然语言搜索

原创
作者头像
用户11460993
修改2025-03-19 09:15:30
修改2025-03-19 09:15:30
5620
举报

什么是Eino

Eino 是字节跳动开源的大模型应用开发框架,拥有稳定的内核,灵活的扩展性,完善的工具生态,可靠且易维护,背靠豆包、抖音等应用的丰富实践经验。Eino 基于明确的「组件」定义,提供强大的流程「编排」,覆盖开发全流程,旨在帮助开发者以最快的速度实现最有深度的大模型应用。

项目地址:

Eino 组件

Eino 应用的基本构成元素是功能各异的组件,就像足球队由不同位置角色的队员组成。Eino 的开发过程中,首先要做的是决定 “我需要使用哪个组件抽象”,再决定 “我需要使用哪个具体组件实现”。就像足球队先决定 “我要上 1 个前锋”,再挑选 “谁来担任这个前锋”。组件可以像使用任何的 Go interface 一样单独使用。但要想发挥 Eino 这支球队真正的威力,需要多个组件协同编排,成为一个相互联结的整体。

Eino编排方式

在 Eino 编排场景中,每个组件成为了 “节点”(Node),节点之间 1 对 1 的流转关系成为了 “边”(Edge),N 选 1 的流转关系成为了 “分支”(Branch)。基于 Eino 开发的应用,经过对各种组件的灵活编排,就像一支足球队可以采用各种阵型,能够支持无限丰富的业务场景。

足球队的战术千变万化,但却有迹可循,有的注重控球,有的简单直接。对 Eino 而言,针对不同的业务形态,也有更合适的编排方式。

什么是Infortress

Infortress是应用软件型NAS解决方案,官方网站为 https://hardstones.com 用户只需要在自己的电脑上安装一个应用软件就能给电脑增加NAS功能,不仅如此Infortress还集成了Ollama和AnythingLLM,可以通过图形界面来部署本地大模型和连接知识库,通过Infortress App,用户可以远程访问本地的大模型和知识库。 Infortress由于其本地部署的特性,特别适合扩展AI Agent功能帮助用户提高生产力。作为提供AI功能的第一步,我们尝试了使用Eino和大模型来提供自然语言搜索功能。开始AI时代用AI重写应用的第一步。

自然语言搜索功能

现有搜索功能

Infortress已经提供了搜索文件的功能,用户可以在App界面上选择文件类型,排序方式,然后输入关键字匹配文件名进行搜索。功能展示如下。

自然搜索功能描述

我们希望用户直接在输入框输入,类似“2025年的所有照片安装从大到小排列”这样简短的话,大模型能够将其解析成为一个Golang Struct的搜索条件,然后我们的后台程序使用这个搜索条件,找到相应的照片并排序。

点击链接查看 Infortress 自然语言搜索功能展示视频

技术需求

对于当前的搜索功能,在Infortress客户端,我们会将用户的输入转换为如下的请求发送给服务端。

代码语言:txt
复制
type PageRequest struct {
	Size    int    `json:"size"`
	Index   int    `json:"index"`
	OrderBy string `json:"orderBy"`
	Sort    string `json:"sort"`
}

type SearchRequest struct {
	Pager                  PageRequest `json:"pager"`
	FileTypes              string      `json:"fileTypes"`
	FileExtensions         string      `json:"fileExtensions"`
	FileNameFuzzyMatch     string      `json:"fileNameFuzzyMatch"`
	ModifyTimeStart        int64       `json:"modifyTimeStart"` //millseconds since EPOCH
	ModifyTimeEnd          int64       `json:"modifyTimeEnd"`   //millseconds since EPOCH
	SizeLow                int64       `json:"sizeLow"`
	SizeHigh               int64       `json:"sizeHigh"`
	Deleted                int         `json:"deleted"`         //0: ignore, 1: true, -1: false
}

然后后台程序会用这些条件在数据库搜索。这是很常规的一个实现方案。

要支持自然语言搜索,那么我们就只需要大模型将用户的自然语言描述,转换为这样一个结构,然后我们就重用数据库搜索功能来查找用户需要的文件就可以了。

例如用户如果输入“2025年的所有照片安装从大到小排列” ,大模型需要将其转换为下列搜索条件,然后在数据库查询。

代码语言:txt
复制
{
  "pager": {
    "size": 10,
    "index": 1,
    "orderBy": "size",
    "sort": "desc"
  },
  "fileTypes": "image",
  "fileExtensions": "",
  "fileNameFuzzyMatch": "",
  "modifyTimeStart": 1735660800000,
  "modifyTimeEnd": 1767196800000,
  "sizeLow": 0,
  "sizeHigh": 0,
  "deleted": -1
}

技术实现

为了支持将自然语言转换为golang的搜索请求。我们需要用到Eino的Tool组件,另外一个必不可少的组件是ChatModel用来调用大模型,然后通过Chain来编排整个操作。直接上完整的代码,您对照注释应该能很容易理解代码。

代码语言:txt
复制
package aiAgents

import (
	"context"
	"encoding/json"
	"infortresserver/logger"
	"infortresserver/models"

	"github.com/cloudwego/eino-ext/components/model/ark"
	"github.com/cloudwego/eino/components/tool"
	"github.com/cloudwego/eino/components/tool/utils"
	"github.com/cloudwego/eino/compose"
	"github.com/cloudwego/eino/schema"
)

var (
	// 声明一个全局的Agent变量
	agent *compose.Runnable[[]*schema.Message, []*schema.Message] = nil
	// 大模型的配置,这个如果是提供互联网服务的话可以直接写死
	activeLLM models.LLMConfig
)

// 定义大模型输出的结构 description 字段是给大模型的提示prompt,告诉大模型该怎么生成对应的值
type FileSearchRequest struct {
	OrderBy            string `json:"orderBy" jsonschema:"description=排序方式,按文件名排序返回name,按文件类型或者扩展名排序返回extension,按时间排序返回modifyTime,按文件大小排序返回size,默认返回name"`
	Sort               string `json:"sort" jsonschema:"description=文件排序的顺序,返回asc或者desc,默认asc,"`
	FileNameFuzzyMatch string `json:"fileNameFuzzyMatch,omitempty" jsonschema:"description=文件名或者文件名的一部分"`
	FileTypes          string `json:"fileTypes" jsonschema:"description=文件类型,有image,video,audio,document,这几种,如果没找到则返回空字符串,如果包含多个类型则以|连接"`
	FileExtensions     string `json:"fileExtensions,omitempty" jsonschema:"description=文件扩展名,比如doc,pdf,xls,mp4,mp3等,如果没找到则返回空字符串,如果包含多个类型则以|连接"`
	ModifyTimeStart    int64  `json:"modifyTimeStart,omitempty" jsonschema:"description=时间范围的启始时间,为millisecond since epoc"`
	ModifyTimeEnd      int64  `json:"modifyTimeEnd,omitempty" jsonschema:"description=时间范围的结束时间,为millisecond since epoc"`
	// SizeLow            int64  `json:"sizeLow,omitempty" jsonschema:"description=文件需要大于这个值,单位为字节"`
	// SizeHigh           int64  `json:"sizeHigh,omitempty" jsonschema:"description=文件需要小于这个值,单位为字节"`
}

func BuildFileSearchRequest(_ context.Context, params *FileSearchRequest) (*FileSearchRequest, error) {
	//Eino 在大模型生成params后会执行这个函数
	//我们这里不做任何操作,直接返回params给程序去做进一步处理
	return params, nil
}

// 初始化Agent
func initAgent(llm models.LLMConfig) {

	ctx := context.Background()

	// 通过Eino的 工具类utils生成一个tool,Eino还提供了tool定义的其他方式,请参考Eino文档
	// 第一个参数是tool的名字,第二个参数是给大模型关于这个tool的角色设定
	// 第三个参数是一个函数,这个函数有2个意义,函数的参数是大模型要根据用户输入转换成的golang结构,tool在调用大模型生成golang结构后,会执行这个函数
	searchRequestBuilder, err := utils.InferTool("file_searcher_request_builder", "你的任务是根据用户的描述,提取搜索文件的过滤条件信息", BuildFileSearchRequest)
	if err != nil {
		logger.Log.Errorf("InferTool failed, err=%v", err)
		return
	}

	// 初始化tools,支持多个tool的,我们这里只用到了一个tool
	fileSearchTools := []tool.BaseTool{
		searchRequestBuilder,
	}

	activeLLM = llm
	agent = nil

	// 创建并配置 ChatModel 如果是提供互联网服务的话可以大模型参数可以直接写死
	chatModel, err := ark.NewChatModel(context.Background(), &ark.ChatModelConfig{
        // 这里必须选择支持function call的大模型,deepseek目前不支持
		// BaseURL: "https://ark.cn-beijing.volces.com/api/v3", // 服务地址
		// Region:  "cn-beijing",                               // 区域
		// Model:   "doubao-pro-32k-functioncall-240815",       // 模型端点 ID
		// APIKey:  "dddddd-hhh-gggg-ffff-eeeeeeee",
		BaseURL: llm.BaseURL, // 服务地址
		Region:  llm.Region,  // 区域
		Model:   llm.Model,   // 模型端点 ID
		APIKey:  llm.APIKey,
		// Temperature: gptr.Of(float32(0.7)),
	})
	if err != nil {
		logger.Log.Errorf("NewChatModel failed, err=%v", err)

		return
	}

	// 获取工具信息, 用于绑定到 ChatModel
	toolInfos := make([]*schema.ToolInfo, 0, len(fileSearchTools))
	var info *schema.ToolInfo
	for _, fileSearchTool := range fileSearchTools {
		info, err = fileSearchTool.Info(ctx)
		if err != nil {
			logger.Log.Infof("get ToolInfo failed, err=%v", err)
			return
		}
		toolInfos = append(toolInfos, info)
	}

	// 将 tools 绑定到 ChatModel
	err = chatModel.BindTools(toolInfos)
	if err != nil {
		logger.Log.Errorf("BindTools failed, err=%v", err)
		return
	}

	// 创建 tools 节点
	fileSearchToolsNode, err := compose.NewToolNode(context.Background(), &compose.ToolsNodeConfig{
		Tools: fileSearchTools,
	})
	if err != nil {
		logger.Log.Errorf("create fileSearchToolsNode failed, err=%v", err)
		return
	}

	// 构建完整的处理链
	chain := compose.NewChain[[]*schema.Message, []*schema.Message]()
	chain.
		AppendChatModel(chatModel, compose.WithNodeName("chat_model")).
		AppendToolsNode(fileSearchToolsNode, compose.WithNodeName("file_search_tools_node"))

	// 编译并运行 chain
	newAgent, err := chain.Compile(ctx)
	if err != nil {
		logger.Log.Errorf("chain.Compile failed, err=%v", err)
		return
	}

	agent = &newAgent

}

// 这个函数给外部程序调用,输入为用户的自然语言,输出为大模型转换后的golang struct搜索过滤条件
func BuildSearchRequestFromNatureLanguage(llm models.LLMConfig, input string) *FileSearchRequest {

	ctx := context.Background()

	//初始化agent
	if agent == nil || llm != activeLLM {
		initAgent(llm)
	}
	// 运行Agent
	resp, err := (*agent).Invoke(ctx, []*schema.Message{
		{
			Role:    schema.User,
			Content: input,
		},
	})
	if err != nil {
		logger.Log.Errorf("agent.Invoke failed, err=%v", err)
	}

	// // 输出结果
	for idx, msg := range resp {
		logger.Log.Infof("\n")
		logger.Log.Infof("message %d: %s: %s", idx, msg.Role, msg.Content)

		fileSearchRequest := FileSearchRequest{}
		//大语言的输出是Json格式的FileSearchRequest,我们这里序列化一下
		//不知道有没有办法直接返回golang struct,知道的同学指教一下
		json.Unmarshal([]byte(msg.Content), &fileSearchRequest)
		return &fileSearchRequest
	}

	return nil

}

外部调用

代码语言:txt
复制
type SearchResult struct {
	Request models.SearchRequest `json:"request"`
	Files   []db.FileEntry       `json:"files"`
}

func searchFilesV2(ctx iris.Context) {

	logger.Log.Debug("searchFiles reqeust got")

	var searchRequest models.SearchRequest

	if err := ctx.ReadJSON(&searchRequest); err != nil {
		logger.Log.Error(errors.Wrap(err, "decode search request failed."))
		ctx.StatusCode(iris.StatusBadRequest)
		return
	}


	applyFiltersGeneratedFromNatureLanguage(&searchRequest)

	lrs, err := db.SearchFiles(searchRequest)
	if err != nil {
		logger.Log.Error(errors.Wrap(err, "search files faied"))
	}

	result := SearchResult{}
	//返回SearchRequest方便客户端加载更多条目时直接使用
	result.Request = searchRequest
	result.Files = lrs

	ctx.JSON(result)

}



func applyFiltersGeneratedFromNatureLanguage(searchRequest *models.SearchRequest) *models.SearchRequest {

	//如果搜索请求不是自然语言,则直接使用客户端的搜索条件,走常规搜索方式
	if searchRequest.NaturalLanguageContent == "" {
		return searchRequest
	}
	
	//调用AiAgent 根据用户自然语言输入生成搜索条件
	searchFileFilter := aiAgents.BuildSearchRequestFromNatureLanguage(searchRequest.LLM, searchRequest.NaturalLanguageContent)
	if searchFileFilter == nil {
		return searchRequest
	}

	//将搜索条件附加到客户端传入的搜索条件
	if searchFileFilter.Sort != "" {
		searchRequest.Pager.Sort = searchFileFilter.Sort
	}

	if searchFileFilter.OrderBy != "" {
		searchRequest.Pager.OrderBy = searchFileFilter.OrderBy
	}

	if searchFileFilter.FileNameFuzzyMatch != "" {
		searchRequest.FileNameFuzzyMatch = searchFileFilter.FileNameFuzzyMatch
	}

	if searchFileFilter.FileNameFuzzyMatch != "" {
		searchRequest.FileNameFuzzyMatch = searchFileFilter.FileNameFuzzyMatch
	}

	if searchFileFilter.FileTypes != "" {
		searchRequest.FileTypes = searchFileFilter.FileTypes
	}

	if searchFileFilter.FileExtensions != "" {
		searchRequest.FileExtensions = searchFileFilter.FileExtensions
	}

	if searchFileFilter.ModifyTimeStart != 0 {
		searchRequest.ModifyTimeStart = searchFileFilter.ModifyTimeStart
	}

	if searchFileFilter.ModifyTimeEnd != 0 {
		searchRequest.ModifyTimeEnd = searchFileFilter.ModifyTimeEnd
	}
	// if searchFileFilter.SizeLow != 0 {
	// 	searchRequest.SizeLow = searchFileFilter.SizeLow
	// }

	// if searchFileFilter.SizeHigh != 0 {
	// 	searchRequest.SizeHigh = searchFileFilter.SizeHigh
	// }

	return searchRequest

}

展望

Infortress目前已经集成了Ollama支持本地部署大模型,有些大模型比如Ollama的qwen系列是支持function call的,但是由于本地部署的大模型参数不可能太大,还不够智能,暂时还难以支持生产环境的应用,等以后大模型功能进一步进化,或许很快本地模型也可以支持精确的function call的调用,那么Infortress就可以直接用用户的本地大模型,而不用公网模型来支持这个功能,既解决了用户的隐私担忧,也省去了调用公网模型的成本。

Infortress作为本地部署的典型解决方案,后续我们将继续探索使用Eino来开发本地知识库和其他功能。如开发Agent来完成发送邮件,自动下载,查找磁盘文件及其他更多的操作。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是Eino
    • Eino 组件
    • Eino编排方式
  • 什么是Infortress
  • 自然语言搜索功能
    • 现有搜索功能
    • 自然搜索功能描述
    • 技术需求
  • 技术实现
    • 外部调用
  • 展望
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档