当前位置: 首页 > news >正文

【极客日常】用Eino+Ollama低成本研发LLM的Agent

十一国庆正是充电的好时机,借着假期时间充裕,笔者又浅调研了一下本地LLM开发相关的工具链,看下如果是日常业余搞个人LLM的Agent项目,具体有哪些能力可用。工业界的话,因为知识保密性等各种原因,我们可能会用到兄弟部门的LLM模型或者相关Agent能力,以及市面上收费但企业内部免费的一些技术基建。但如果是个人搞LLM应用开发,就更加倾向于看有没有低成本甚至免费的办法去做本地研发了。

基于这个目的,经过一番调研实操,发现只需要一个Agent开发框架加上模型Provider就能解决问题。因此本文就介绍一下,以Agent开发框架Eino,加上Ollama这个模型Provider,如何能够低成本研发LLM的Agent。针对这个主题,虽然以前也写过用Coze开源版研发的Case,但Coze本身作为一套工业界产品基建,直接拿它工作还是比较重的,本文暂且只讨论一些比较轻量的事情。

首先咱们需要理解模型对标现实中的啥,具体怎么提升生产力。按笔者粗浅理解的话,一个模型实例就相当于一个大脑,它节省开发者工作量的地方在于,以前的程序是开发者一行行代码编写出来的,而现在我们可以通过微调或者工具增强等方式定制化一个大脑,使得在尽可能减少确定性折损的条件下,低成本做多模态的数据转换,甚至实现另一套我们需要的程序。不管这个理解是不是精确,但至少有了这个想法的话,开发一个LLM应用思路会清晰的多。

在本地,我们可以借助Ollama工具管理多个大脑,每个大脑有不同的能力,比如gemma3可以处理视觉信息,qwen3可以做外部工具识别,bge-m3可以做文本向量化(embedding),deepseek-r1具备自思考能力,然后基本上每个模型都有问答能力,等等。在具体实现上,我们可以组合不同的模型,打造一套完善的Agent。

比方说有用户问,想要去某个图片里面的地方旅游,有什么方案?那么我们的Agent可以实现成,首先借助deepseek-r1的思考能力做意图识别,发现问题包含额外图片信息,之后就调用gemma3模型(或是封装的Agent)做图片识别,识别图片里的关键地标信息,再之后结合向量数据库跟我们通过bge-m3模型embed的大量文本,我们可以构建一套地理知识库,在这个知识库里检索到这个地标对应的城市,最后再借助qwen3以及外部高德地图等工具,规划出一套完整的旅行方案,回给主脑deepseek-r1吐出来。具体怎么管理Ollama的模型,可以参考Ollama官方文档。

为了实现这样的编排,我们需要有一套Agent开发框架,常见的就是基于Python的LangChain以及基于Golang的Eino。本文以Eino为例子,Eino内部有封装对Ollama的调用,所以通过Eino连接Ollama模型也比较简单,示例代码:

func (a *EinoOllamaAgent) Run(ctx context.Context) {// connect local ollama modelmodel, err := ollama.NewChatModel(ctx, &ollama.ChatModelConfig{// 基础配置BaseURL: ollamaURL,        // Ollama 服务地址,通常为http://localhost:11434Timeout: 30 * time.Second, // 请求超时时间// 模型配置Model:  qwen3Model,                // 模型名称,比如qwen3:latestFormat: json.RawMessage(`"json"`), // 输出格式(可选)// 模型参数Options: &api.Options{Temperature: 0.7,NumPredict:  8192,},// 推理配置Thinking: &api.ThinkValue{Value: false},})if err != nil {panic(errors.Errorf("create ollama chat model failed: %v", err))}messages := []*schema.Message{schema.SystemMessage("你是一个助手"),schema.UserMessage("请用一句话介绍Ollama"),}// 普通模式response, err := model.Generate(ctx, messages)if err != nil {panic(errors.Errorf("generate msg failed: %v", err))}fmt.Printf("resp: %s\n", response.Content)
}

如果是需要构建知识库的场景,那么我们需要做的一是把embedding模型当成通用文本向量化工具,不单独写一套代码,二是引入一个向量数据库,持久化文本向量,提供知识访问能力。如果用Eino实现的话,先给一个以内存作为向量数据库的最简单例子,当然Eino本身也有很多向量数据库Client的抽象,此处不赘述了。

type Doc struct {ID        intContent   stringEmbedding []float64
}type EinoOllamaKnowledge struct {docs     map[int]*Docembedder *openai.EmbedderidIncr   intidMtx    sync.Mutex
}func NewEinoOllamaKnowledge(ctx context.Context) *EinoOllamaKnowledge {embedder, err := openai.NewEmbedder(ctx, &openai.EmbeddingConfig{BaseURL: ollamaV1URL, // Ollama服务v1地址,兼容OpenAI接口Model:   embedModel, // 模型名称,比如bge-m3:latestTimeout: 30 * time.Second,})if err != nil {panic(errors.Errorf("create ollama embed model failed: %v", err))}return &EinoOllamaKnowledge{docs:     make(map[int]*Doc),embedder: embedder,}
}func (k *EinoOllamaKnowledge) Run(ctx context.Context) {texts := []string{"床前明月光,疑是地上霜。举头望明月,低头思故乡。","离离原上草,一岁一枯荣。野火烧不尽,春风吹又生。","白日依山尽,黄河入海流。欲穷千里目,更上一层楼。","煮豆燃豆萁,豆在釜中泣。本是同根生,相煎何太急。","鹅鹅鹅,曲项向天歌。白毛浮绿水,红掌拨清波。",}k.AddDocs(ctx, texts)queries := []string{"韧性","登高","夜晚","动物","兄弟",}for _, q := range queries {doc := k.FindMostSimilarDoc(ctx, q)if doc != nil {fmt.Printf("query: %s, most similar doc: %s\n", q, doc.Content)} else {fmt.Printf("query: %s, no similar doc found\n", q)}}
}func (k *EinoOllamaKnowledge) genID() int {k.idMtx.Lock()defer k.idMtx.Unlock()k.idIncr++return k.idIncr
}func (k *EinoOllamaKnowledge) AddDocs(ctx context.Context, texts []string) {embeddings, err := k.embedder.EmbedStrings(ctx, texts)if err != nil {panic(errors.Errorf("generate embedding failed: %v", err))}if len(embeddings) != len(texts) {panic(errors.Errorf("embedding count not equal to text count: %d != %d", len(embeddings), len(texts)))}for i := 0; i < len(texts); i++ {id := k.genID()doc := &Doc{ID:        id,Content:   texts[i],Embedding: embeddings[i],}k.docs[id] = doc}
}func (k *EinoOllamaKnowledge) GetDoc(id int) *Doc {if doc, ok := k.docs[id]; ok {return doc}return nil
}// FindMostSimilarDoc 最简单的查找最相似文档的实现
func (k *EinoOllamaKnowledge) FindMostSimilarDoc(ctx context.Context, text string) *Doc {if text == "" || len(k.docs) == 0 {return nil}embeddings, err := k.embedder.EmbedStrings(ctx, []string{text})if err != nil {panic(errors.Errorf("generate embedding failed: %v", err))}if len(embeddings) != 1 {panic(errors.Errorf("embedding count not equal to text count: %d != %d", len(embeddings), 1))}queryEmbedding := embeddings[0]cosineSimilarity := func(a, b []float64) float64 {if len(a) != len(b) {return 0}var dotProduct, normA, normB float64for i := range a {dotProduct += a[i] * b[i]normA += a[i] * a[i]normB += b[i] * b[i]if normA == 0 || normB == 0 {return 0}}return dotProduct / (math.Sqrt(normA) * math.Sqrt(normB))}var mostSimilar *DocmaxScore := -1.0for _, doc := range k.docs {score := cosineSimilarity(queryEmbedding, doc.Embedding)fmt.Printf("[FindMostSimilarDoc] query: %s, doc: %s, score: %v\n", text, doc.Content, score)if score > maxScore {maxScore = scoremostSimilar = doc}}return mostSimilar
}

值得一提的是,如果这段代码转成Python也是比较容易的,比如Trae这种善于处理代码任务的Agent就可以做不同语言代码转换。假使用LangChain实现,外加ChromaDB本地持久化向量文本的话,可以这样写:

from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.schema import Document
import uuid
import os
import shutilclass OllamaKnowledge:def __init__(self, model="bge-m3:latest", ollama_base_url="http://localhost:11434",persist_directory="./chroma_db"):# 初始化Ollama嵌入模型self.embeddings = OllamaEmbeddings(model=model,base_url=ollama_base_url)# 初始化Chroma向量存储self.vector_store = Chroma(embedding_function=self.embeddings,persist_directory=persist_directory)def add_docs(self, texts):"""添加文档到向量数据库"""documents = []for text in texts:# 为每个文档生成唯一IDdoc_id = str(uuid.uuid4())# 创建LangChain文档对象document = Document(page_content=text,metadata={"id": doc_id})documents.append(document)# 将文档添加到向量存储self.vector_store.add_documents(documents)# 持久化存储self.vector_store.persist()def find_most_similar_doc(self, query, k=1):"""查找与查询最相似的文档"""if not query:return None# 执行相似度搜索results = self.vector_store.similarity_search_with_score(query, k=k)if not results:return None# 返回最相似的文档most_similar_doc, score = results[0]return most_similar_doc, scoredef run_demo(self):"""运行演示:添加文档并执行查询"""# 示例文档(唐诗)texts = ["床前明月光,疑是地上霜。举头望明月,低头思故乡。","离离原上草,一岁一枯荣。野火烧不尽,春风吹又生。","白日依山尽,黄河入海流。欲穷千里目,更上一层楼。","煮豆燃豆萁,豆在釜中泣。本是同根生,相煎何太急。","鹅鹅鹅,曲项向天歌。白毛浮绿水,红掌拨清波。",]# 添加文档print("正在添加文档到向量数据库...")self.add_docs(texts)print(f"成功添加了 {len(texts)} 篇文档\n")# 查询示例queries = ["韧性", "登高", "夜晚", "动物", "兄弟"]for q in queries:result = self.find_most_similar_doc(q)if result:doc, score = resultprint(f"查询: {q}")print(f"最相似的文档: {doc.page_content}")print(f"相似度得分: {score:.4f}\n")else:print(f"查询: {q}, 未找到相似文档\n")# 主函数
if __name__ == "__main__":# 创建OllamaKnowledge实例knowledge = OllamaKnowledge()# 运行演示knowledge.run_demo()

对于复杂编排,除了可以考虑用Dify之类的可视化工具做之外,纯程序的话,Eino也提供了一套ADK框架封装了更复杂的Agent编排功能。除了最基础的ChatModelAgent之外,再往上实现的是WorkflowAgents,里面包括Sequential、Loop以及Parallel等编排,也就是行为树的翻版,然后再继续往上就实现了Supervisor以及Plan-Execute两类封装好的应用级编排。

对于调研类任务的话,有一个封装好的Plan-Execute编排,加上靠谱的数据处理模型,就可以实现一个简单的调研类Agent:

type EinoAdkAgent struct {runner *adk.Runner
}func NewEinoAdkAgent() *EinoAdkAgent {a := &EinoAdkAgent{}if err := a.init(context.Background()); err != nil {panic(errors.Errorf("initialize EinoAdkAgent failed: %v", err))}return a
}func (a *EinoAdkAgent) Run(ctx context.Context) {userInput := []adk.Message{schema.UserMessage("请用中文回答如何写一篇100000字的科幻小说?"),}events := a.runner.Run(ctx, userInput)for {event, ok := events.Next()if !ok {break}if event.Err != nil {log.Printf("执行错误: %v", event.Err)break}// 打印智能体输出(计划、执行结果、最终响应等)if msg, err := event.Output.MessageOutput.GetMessage(); err == nil && msg.Content != "" {log.Printf("\n=== Agent [%s] Output ===\n%s\n", event.AgentName, msg.Content)}}
}func (a *EinoAdkAgent) init(ctx context.Context) error {// init chat modelchatModel, err := a.initChatModel(ctx)if err != nil {return errors.Errorf("create ollama chat model failed: %v", err)}var agent adk.Agent// init plan-executorplanExecutor, err := a.initPlanExecutor(ctx, chatModel)if err != nil {return errors.Errorf("create plan-executor agent failed: %v", err)}agent = planExecutor// init runnera.runner = adk.NewRunner(ctx, adk.RunnerConfig{Agent: agent, EnableStreaming: true})return nil
}func (a *EinoAdkAgent) initChatModel(ctx context.Context) (model.ToolCallingChatModel, error) {return ollama.NewChatModel(ctx, &ollama.ChatModelConfig{// 基础配置BaseURL: ollamaURL,         // Ollama 服务地址Timeout: 300 * time.Second, // 请求超时时间// 模型配置Model: qwen3Model, // 模型名称// Format: json.RawMessage(`"json"`), // 输出格式(可选)// 模型参数Options: &api.Options{NumPredict: 4096,},// 推理配置Thinking: &api.ThinkValue{Value: false},})
}func (a *EinoAdkAgent) initPlanExecutor(ctx context.Context, chatModel model.ToolCallingChatModel) (adk.Agent, error) {// init plannerplanner, err := planexecute.NewPlanner(ctx, &planexecute.PlannerConfig{ToolCallingChatModel: chatModel,ToolInfo:             &planexecute.PlanToolInfo, // 默认 Plan 工具 schema})if err != nil {return nil, errors.Errorf("create planner agent failed: %v", err)}// init executorexecAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{Name:          "AnySolver",Description:   "你是一个专业的解答者,能够为任意问题生成解答方案。",Instruction:   "你只能根据用户的问题,生成具体可执行的解答方案,不能生成任何与问题无关的内容。",Model:         chatModel,MaxIterations: 1,})if err != nil {return nil, errors.Errorf("create executor chat model agent failed: %v", err)}execTool := adk.NewAgentTool(ctx, execAgent) // 一个纯ChatModel占位,MCP基本收费,先不管executor, err := planexecute.NewExecutor(ctx, &planexecute.ExecutorConfig{Model:         chatModel,MaxIterations: 3,ToolsConfig: adk.ToolsConfig{ToolsNodeConfig: compose.ToolsNodeConfig{Tools: []tool.BaseTool{execTool},},},})if err != nil {return nil, errors.Errorf("create executor agent failed: %v", err)}// init replannerreplanner, err := planexecute.NewReplanner(ctx, &planexecute.ReplannerConfig{ChatModel: chatModel,})if err != nil {return nil, errors.Errorf("create replanner agent failed: %v", err)}// init plan-executor agentplanExecuteAgent, err := planexecute.New(ctx, &planexecute.Config{Planner:       planner,Executor:      executor,Replanner:     replanner,MaxIterations: 10,})if err != nil {return nil, errors.Errorf("create plan-execute agent failed: %v", err)}return planExecuteAgent, nil
}

最后,如果说要把Agent效果继续优化的话,先是要有一套完善的评测系统,然后也需要有一个Trace工具了解整个Agent链路上的弱点,最后可以从工具、Prompt、模型FineTune等很多角度去做优化,从而不断完善Agent的能力。要实现一个Demo很容易,但打磨产品的任务仍然任重道远。

http://www.dtcms.com/a/449338.html

相关文章:

  • 《深入 Django ORM:select_related 与 prefetch_related 的实战剖析与性能优化指南》
  • 男科医院网站模板视频加字幕软件app
  • 网站开发自荐信江门专业网站制作费用
  • nat address-group 概念及题目
  • 深度学习模型构建的本质——“核心四要素+任务适配逻辑”
  • 基于SpringBoot+Vue的志行交通法规在线模拟考试(AI问答、WebSocket即时通讯、Echarts图形化分析、随机测评)
  • 厦门建网站费用一览表网站设计流行趋势
  • Docker Compose 搭建 LNMP 环境并部署 WordPress 论坛
  • 无锡企业网站制作哪家好前端的网站重构怎么做
  • TensorFlow2 Python深度学习 - 深度学习概述
  • Davor的北极探险资金筹集:数学建模与算法优化(洛谷P4956)
  • Web Components 的开发过程举例
  • 【Algorithm】Day-1
  • 提示工程深度解析:驾驭大语言模型的艺术与科学
  • 网站开发证书是什么中国建设学会查询网站
  • java代码随想录day50|图论理论基础
  • 【模型量化迁移】详解:让AI大模型在端侧“轻装上阵”的核心技术
  • 【Proteus仿真】虚拟终端出现乱码问题解决
  • 深入理解HarmonyOS ArkTS语法:从基础到高级应用开发
  • Photoshop - Photoshop 工具栏(5)多边套索工具
  • 做彩票网站空间去哪买网站主播
  • JavaWeb--Ajax
  • 网站建设与维护报告总结许昌网站建设汉狮套餐
  • [初学C语言]关于scanf和printf函数
  • Oracle OCP认证考试题目详解082系列第2题
  • c++中<iostream> 常用接口汇总
  • Photoshop - Photoshop 工具栏(6)对象选择工具
  • 爱发电nginx转发企业微信webhook
  • 四川红叶建设有限公司网站长沙专业做网站
  • 光通信|模分复用技术-综述