Eino 是字节跳动开源的大模型应用开发框架,拥有稳定的内核,灵活的扩展性,完善的工具生态,可靠且易维护,背靠豆包、抖音等应用的丰富实践经验。Eino 基于明确的「组件」定义,提供强大的流程「编排」,覆盖开发全流程,旨在帮助开发者以最快的速度实现最有深度的大模型应用。
项目地址:
Eino 应用的基本构成元素是功能各异的组件,就像足球队由不同位置角色的队员组成。Eino 的开发过程中,首先要做的是决定 “我需要使用哪个组件抽象”,再决定 “我需要使用哪个具体组件实现”。就像足球队先决定 “我要上 1 个前锋”,再挑选 “谁来担任这个前锋”。组件可以像使用任何的 Go interface 一样单独使用。但要想发挥 Eino 这支球队真正的威力,需要多个组件协同编排,成为一个相互联结的整体。
在 Eino 编排场景中,每个组件成为了 “节点”(Node),节点之间 1 对 1 的流转关系成为了 “边”(Edge),N 选 1 的流转关系成为了 “分支”(Branch)。基于 Eino 开发的应用,经过对各种组件的灵活编排,就像一支足球队可以采用各种阵型,能够支持无限丰富的业务场景。
足球队的战术千变万化,但却有迹可循,有的注重控球,有的简单直接。对 Eino 而言,针对不同的业务形态,也有更合适的编排方式。
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客户端,我们会将用户的输入转换为如下的请求发送给服务端。
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年的所有照片安装从大到小排列” ,大模型需要将其转换为下列搜索条件,然后在数据库查询。
{
"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来编排整个操作。直接上完整的代码,您对照注释应该能很容易理解代码。
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
}
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 删除。