Genkit代理API实战:构建多回合AI代理的完整指南
发布时间:2026/9/30 5:56:30来源:尧图网络
最近在做一个内部客服助手需要支持多轮对话、查询订单状态、判断售后策略还要能随时切回本地模型离线跑。折腾了一段时间后我决定用Genkit来做这个多回合AI代理整体体验比我之前裸调模型API要顺得多。这篇文章就围绕“Genkit的代理API”展开把多回合AI代理的设计思路、核心机制、完整实操代码以及我踩过的坑都整理出来适合正在做AI Agent相关项目、想从单轮问答升级到多轮协作的开发者参考。1. 先搞清楚Genkit的代理API到底解决什么问题1.1 多回合AI代理的痛点和需求拆解所谓的多回合AI代理就是AI不只回答用户当前这一句话而是能记住前面聊过什么能根据上下文追问能主动调用外部工具查数据然后在“对话-推理-行动-再对话”之间来回切换直到把问题解决。比如用户问“我上周买的东西到哪了”代理需要先知道用户是谁、是哪一笔订单然后查物流系统如果物流显示异常还要查询售后政策并给出方案。这个过程中每一步都有可能产生新问题、需要再次查询模型必须带着前面的所有信息继续决策。直接裸调大模型API做这种事麻烦是一堆的。你得自己把历史消息数组传来传去自己写工具调用循环自己处理模型返回的tool_call结构自己做会话状态持久化还得应付不同模型提供商差异巨大的请求格式。这些事单做都不难但堆在一起代码量很快就失控了。尤其是当你要让代理在云端模型和本地模型之间切换时接口差异会让人崩溃。所以项目里我需要的不是一个会聊天的接口而是一个能承载“会话状态、工具调用、多回合循环”的代理框架。Genkit正好把这几块都补齐了它能让我把注意力放在业务逻辑上而不是反复造轮子。1.2 为什么选择Genkit而不是裸调模型Genkit是Google开源的AI应用开发框架定位非常明确让开发者用统一的方式调用模型、定义工具、编排流程并且内置了可观测性和开发者工具。它不绑定某一家模型厂商Google的Gemini、Anthropic、本地Ollama、OpenAI兼容接口等都能通过插件接入。这一点对我特别重要因为我需要线上跑云端模型调试时或数据敏感时切换本地模型。我做一个简单对比来看不同方案的差异关注点裸调模型API自己封装Agent轮询Genkit多回合历史管理完全手动拼数组自己实现内置session机制自动维护工具定义与校验按各家格式写自己写parser使用Zod schema定义自动校验工具调用循环需要手动while自己实现有request-response循环封装模型切换改大量代码改调用层换插件即可可观测性无自己打日志自带开发者UI和trace这是很实际的选择逻辑如果你的代理只有一两个回合、不需要复杂工具那裸调完全没问题。但一旦你准备做真正的业务型AI代理Genkit这类框架的收益是非常明显的。它解决的核心问题是“代理的工程化”而不只是“模型调用”。Genkit的代理API也不是凭空造出来的概念。它把整个代理跑起来需要的东西都标准化了模型、工具、上下文、循环。代理可以理解成一个“有手有脚会记忆的对话机器人”Genkit提供的就是给它套上手脚、接上记忆的插座。2. 环境准备与工具选型2.1 项目初始化与依赖安装我使用的是Node.js环境Genkit对TypeScript支持很完善。如果你的项目还没初始化先做基础准备mkdir genkit-agent-demo cd genkit-agent-demo npm init -y然后安装核心依赖npm install genkit-ai/core genkit-ai/flow npm install genkit-ai/google # 云端模型插件 npm install genkitx-ollama # 本地模型插件 npm install zod # 结构化校验 npm install -D genkit-ai/cli # 开发者工具安装完成后用npx genkit init可以直接生成项目模板但我倾向于手动搭建结构更清晰。需要注意的点Genkit要求Node.js 18以上建议用20版本。如果你之前的项目有其他AI依赖版本冲突是常有的事最好在新目录里做依赖隔离。我在实践中还发现genkit-ai/flow和genkit-ai/core的版本必须匹配否则运行时会报奇怪的插件错误。所以第一次安装时建议都用默认的latest版本并且生成好package-lock.json后续升级再统一规划。2.2 模型接入云端API与本地模型两条路Genkit的模型接入是通过插件实现的。我想要的是同一套代理代码既能调用云端模型也能切换到本地模型所以最理想的做法是把模型定义放在一个独立模块中后续随时替换。云端模型我用的是Google的Gemini系列配置很简单import { googleAI } from genkit-ai/google; import { genkit } from genkit-ai/core; const ai genkit({ plugins: [ googleAI({ apiKey: process.env.GOOGLE_API_KEY }) ], model: googleAI.models.geminiPro(), });环境变量GOOGLE_API_KEY放到.env里Genkit会自动加载。有一点要特别提醒密钥千万别写进代码里尤其不要提交到Git仓库。我习惯在项目根目录建一个.env.example文件把需要的环境变量列出来这样团队成员clone下来就知道要配什么。本地模型方面我使用Ollama。Ollama是一个本地运行大模型的工具一条命令就能拉起一个模型服务特别适合离线场景ollama pull qwen2.5:7b ollama serve然后在Genkit里接入import { ollama } from genkitx-ollama; const ai genkit({ plugins: [ ollama({ servers: [{ baseURL: http://localhost:11434 }] }) ], model: ollama.models.qwen2_5(), });这里要注意不同的ollama模型命名可能不同使用前最好先用ollama list查看本地已经拉取的模型名称再根据genkitx-ollama支持的方式去引用。我当时用llama3.1时模型名称是llama3.1换成qwen2.5时就要变成qwen2.5或者qwen2_5具体看插件版本。如果引用不存在的模型名调用时会直接报错。云端模型和本地模型两者各有优势。云端的推理能力强、工具调用更稳定适合生产本地模型无网络依赖、数据不出本地适合做开发和敏感性较高的场景。在Genkit里切换它们其实就是换插件配置代理逻辑完全不用改。3. 多回合代理的核心机制拆解3.1 会话状态与会话历史管理多回合AI代理的核心是“记忆”。想象一下你去窗口办事如果接待员不记得你上一句话说了什么你每次都得从头解释那效率得多差。多回合代理的记忆就是会话状态它至少包含两部分对话历史消息列表以及当前任务的中间状态比如已查到的订单号、已选择的处理方案。Genkit处理会话的方式并不是强制规定而是提供了session相关的抽象让你可以自己管理上下文。我实际用的方案是把历史消息按会话ID存到Redis里每次请求到达时从Redis取出历史消息追加用户新消息组成一个完整的消息数组再交给模型。这里有一个特别重要的技术细节消息数组中需要有不同类型的消息。通常包括system系统提示、user用户输入、model模型回复、tool工具返回结果。模型会基于完整数组理解上下文。如果你只是把用户的聊天文本拼在一起传进去模型很难分清哪些是历史、哪些是当前请求回答就会混乱。我也会设置上限比如最多保留最近30条消息。因为对话越长token消耗越大响应越慢。更长的历史可以通过摘要来压缩但这会引入信息丢失的问题。具体策略我下面踩坑环节会详细说。3.2 工具调用与代理循环工具调用是代理区别于普通聊天机器人的关键。没有工具的模型只会“动嘴”有了工具它才能“动手”。我给你打个比方用户问“帮我查一下尾号8832的订单到哪了”模型本身不知道订单数据它应该生成“有一个工具可以查订单物流参数是订单号”而不是瞎编一个物流地址。然后代理框架把这个工具请求执行掉拿到真实结果再把这个结果作为上下文交回给模型让模型基于真实数据继续回答。这个过程就是著名的代理循环。伪代码大致是把系统提示、历史消息、当前用户输入交给模型模型返回结果可能是文本回复也可能是工具调用请求如果是工具调用请求代理执行对应工具把工具执行结果附加到消息序列中再次发给模型重复步骤2-4直到模型返回纯文本回复或达到最大循环次数。这里最容易被忽略的是工具执行结果本身也要成为对话历史的一部分。很多初学者把工具执行结果直接丢掉下次模型就不知道刚才查到了什么自然就无法给出连贯答案。正确的做法是把工具结果作为一个消息追加到对话上下文中让代理时刻“记得”自己已经做过哪些查询。3.3 Genkit中的代理流程配置Genkit里有一个核心概念叫Flow它把输入、输出、业务步骤、错误处理统一封装起来。你可以把Flow理解为代理的“外包装”让代理可以被HTTP调用、被命令行调用、也可以嵌入到任意Node.js服务中。配合Genkit的defineFlow我再结合自定义的代理循环就能得到一个生产可用的代理服务。在Genkit的生态里代理API并不只是一段变量名而是一个相对完整的抽象层它帮你把模型调用、工具注册、上下文组装这几件事绑定到同一个流程里。虽然我们可以手写循环但Genkit也提供了很多工具方法比如针对消息的辅助函数、模型生成的封装这让代码更简洁也更不容易出错。我下面的实操部分会完整展示这个流程。4. 实操用Genkit的代理API构建一个多回合订单助手4.1 定义业务工具我先拿一个具体的案例来做订单助手。这个代理能查订单状态、能查退货政策并且能基于多次查询的结果给出综合性答复。第一步是定义工具。工具本质上是一个函数包含名称、描述、参数Schema和业务实现。描述非常重要因为模型就是靠描述来决定“什么时候该调用这个工具”的。描述写得越清楚工具调用准确率越高。我用Zod来定义参数结构import { z } from zod; import { defineTool } from genkit-ai/core; const getOrderStatusTool defineTool({ name: getOrderStatus, description: 根据订单号查询订单的物流状态和当前节点, inputSchema: z.object({ orderId: z.string().describe(订单号形如20240501XXX), }), outputSchema: z.object({ status: z.string(), location: z.string(), estimatedDays: z.number(), }), }, async ({ orderId }) { // 这里替换为真实业务系统的查询逻辑 return { status: 运输中, location: 上海转运中心, estimatedDays: 2, }; });退货政策工具也很简单const getRefundPolicyTool defineTool({ name: getRefundPolicy, description: 查询商品退货政策了解七天无理由退货规则, inputSchema: z.object({ category: z.string().describe(商品类别如数码、服装、食品), }), outputSchema: z.object({ policy: z.string(), windowDays: z.number(), note: z.string(), }), }, async ({ category }) { // 模拟返回政策数据 const policies: Recordstring, any { 数码: { policy: 支持七天无理由退货, windowDays: 7, note: 需保证包装完好 }, 服装: { policy: 支持七天无理由退货, windowDays: 7, note: 吊牌未拆 }, 食品: { policy: 不支持无理由退货, windowDays: 0, note: 食品安全法规定 }, }; return policies[category] || policies[数码]; });这里有几个实战要点。第一inputSchema里每个字段的describe文字不要省略它是模型理解参数含义的来源。第二工具实现要尽量做异常处理比如订单号不存在时抛异常或返回错误状态否则模型可能把异常输出当成正常结果。第三工具返回的数据结构要简单嵌套太深会让模型难以消化。4.2 编写代理主体与多回合循环有了工具之后接下来是组装代理。我采用的方式是先用Genkit初始化一个实例把模型、工具都注册进去然后定义一个chatFlowFlow在这个Flow内部实现多回合循环。先看初始化代码import { genkit, defineTool } from genkit-ai/core; import { defineFlow } from genkit-ai/flow; import { googleAI } from genkit-ai/google; const ai genkit({ plugins: [googleAI({ apiKey: process.env.GOOGLE_API_KEY })], model: googleAI.models.geminiPro(), tools: [getOrderStatusTool, getRefundPolicyTool], });接下来是核心的循环。我定义了一个chatFlow它的输入是会话ID和用户消息输出是代理最终回复import { z } from zod; export const chatFlow defineFlow({ name: chatFlow, inputSchema: z.object({ sessionId: z.string(), message: z.string(), }), outputSchema: z.object({ reply: z.string(), data: z.any().optional(), }), }, async (input) { const history await getHistory(input.sessionId); history.push({ role: user, text: input.message }); const maxIterations 5; let reply ; for (let i 0; i maxIterations; i) { const response await ai.generate({ messages: history, tools: [getOrderStatusTool, getRefundPolicyTool], config: { temperature: 0.3 }, }); const text response.text(); const toolRequests response.toolRequests; if (!toolRequests || toolRequests.length 0) { reply text; break; } for (const request of toolRequests) { history.push({ role: model, toolCalls: [{ id: request.id, name: request.name, args: JSON.parse(request.input), }], }); } // 执行工具并同步结果 const toolResponses await ai.runTools(toolRequests); for (let j 0; j toolResponses.length; j) { history.push({ role: tool, name: toolResponses[j].name, result: toolResponses[j].output, }); } } if (!reply) { reply 抱歉我暂时无法处理这个请求请稍后重试。; } await saveHistory(input.sessionId, [...history, { role: model, text: reply }]); return { reply }; });这段代码虽然简化了一些但完整展示了代理循环的骨架。有几个细节我想重点解释。ai.generate中的messages是整个会话历史它来自之前存好的历史加上刚收到的用户消息。这样代理才能记住前面聊过什么。toolRequests是模型返回的工具调用请求数组如果为空就代表模型已经可以直接回答了。执行工具这一步我用了Genkit的ai.runTools方法它会根据模型返回的请求自动找到注册过的方法并执行不需要自己写switch-case分发。如果工具执行过程中出错runTools可能会抛出异常因此实际项目中需要在循环里包裹try-catch避免代理流程直接挂掉。另外要注意的是每个工具调用完成后必须把工具结果作为role: tool的消息追加进历史。这一步如果漏了模型在下一次循环时就会“失忆”。我最初写的时候漏掉过结果代理总是重复调用同一个工具后来调试了很久才找到原因。4.3 接入本地模型让代理离线可跑云端模型稳定且聪明但很多场景需要本地化运行比如内网环境、敏感数据、离线演示。Genkit接入Ollama可以说是一行配置的事但真正跑通代理循环还有一些额外工作。首先要切换到本地模型只需把Genkit实例的模型参数改成Ollama即可import { ollama } from genkitx-ollama; const ai genkit({ plugins: [ ollama({ servers: [{ baseURL: http://localhost:11434 }] }) ], model: ollama.models.qwen2_5(), tools: [getOrderStatusTool, getRefundPolicyTool], });然后重新启动服务整个chatFlow不用改一行代理就能用本地模型跑了。但这里有个大坑本地小模型的工具调用能力远不如云端大模型。Gemini、GPT这些模型经过专门训练能很自然地生成结构化工具调用。而7B、13B级别的本地模型经常不按套路出牌可能直接返回一段描述性文字而不是结构化调用请求。Genkit对工具调用做了兼容处理但模型本身不支持的话框架也没办法凭空变出来。我在实操中有一个很管用的折中方案不是强制本地模型输出标准tool_call结构而是让它学会使用一个“describe_action”文本协议。具体做法是把工具描述写得很详细让模型输出类似ACTION: getOrderStatus, orderId20240501XXX的字符串然后我在代理循环层自己解析这个字符串并执行对应函数。这个方法不如原生工具调用优雅但兼容性提高了很多。另一个更省事的办法是选择对工具调用支持较好的本地模型。Qwen系列在工具调用上的表现相对不错模型参数在7B以上时效果才可用。如果你只是做开发调试那完全可以用本地模型但生产级的多回合工具调用我个人建议优先用云端模型本地模型用于回退或数据敏感场景。5. 踩坑记录与排查技巧5.1 多回合上下文丢失问题这个坑我印象最深。一开始我的代理只能正确回答第一轮指令从第二轮开始就会“崩溃”表现为忘记用户之前提供的订单号或者重复询问已经回答过的信息。排查下来发现是历史消息没有正确传递我只把用户新消息发送给模型之前的对话没取出来或者存历史时忘了把模型上一轮的回复也存进去导致历史消息少了一环。解决方案也很简单严格按照system - user - model - user - model的顺序来存。如果涉及工具则插入model(tool_call) - tool(result)。我把完整的消息数组统一存到Redis里每次调用前原样取出跑完后再把新消息追加回去。另外要注意ai.generate的消息对象里字段名要符合Genkit的约定不要自己发明字段名。比如工具消息就必须用role: tool不能写成role: function否则框架无法识别。还有一点很隐蔽当你用Redis存历史时要记得存的是序列化后的完整对象而不是只存文本。因为消息里除了文本还有工具调用、工具结果等结构化字段。如果丢弃结构之后再传给模型模型也无法理解上下文。5.2 工具调用死循环与异常处理代理循环最怕的就是“模型反复调用同一个工具而不给最终答案”。比如查订单状态的工具被调用了5次每次参数都相同模型还是继续请求。这种情况通常是因为工具结果没有正确反馈给模型模型发现自己“看到的”信息还是不全就不断尝试。我在代码里做了一个硬性保护设置最大循环次数默认5次。超过次数直接返回兜底文案。同时在每次循环中会检查工具请求是不是和上一轮的请求完全一样如果一样就中断循环并让模型基于已有信息回答。下面是精简版判断逻辑const lastKey JSON.stringify(toolRequests); if (lastKey previousKey) { reply 根据已有信息我建议您稍后再查或联系人工客服。; break; } previousKey lastKey;另外工具内部可能抛异常比如第三方接口超时。这种异常不能让整个代理挂掉而是要把错误信息作为工具结果返回给模型。例如工具返回{ error: 订单号不存在 }模型看到后就会向用户解释“没有查到该订单”而不是报错。我通常会在所有工具函数的最外层加try-catch把异常转成可解析的结果对象。5.3 本地模型响应格式不兼容本地模型走Genkit时最容易遇到的是响应格式不符合预期。具体表现是模型生成的文本不是JSON或者没有tool_call结构Genkit解析时抛异常。这时候先不要怀疑框架要检查模型本身的能力。我分享一个排查顺序先用curl直接对着Ollama发一个带tools参数的请求看模型能不能返回tool_calls字段。如果连原始接口都拿不到说明模型不支持原生工具调用。如果原始输出有tool_calls但Genkit解析失败多半是消息结构问题检查toolRequests对象的input是不是JSON字符串可能需要对JSON.parse做容错。如果模型完全不会调用工具那就只能采用文本协议让模型以特定字符串格式输出动作指令自己解析执行。本地模型温度参数也要调整。工具调用需要确定性较高的输出温度调到0.2以下明显更稳定。温度高了模型可能每轮输出的格式都不一样循环很容易失控。5.4 性能与成本调优多回合代理对token的消耗比普通聊天大得多。因为每轮循环都要把历史消息重复发送一遍工具调用还会额外增加请求次数。按我的项目统计一个完整的订单查询咨询大约消耗3000~5000 tokens如果对话超过10轮成本会成倍上涨。几个实用的优化方向对历史消息做滑动窗口只保留最近N条消息。对更早的对话做一次摘要并把摘要压缩成一条系统消息。减少不必要的工具调用。只有用户确实提到订单、物流、退货等关键词时才允许模型调用工具。这个可以用系统提示来控制。使用流式输出提升体验但要注意流式输出的工具调用处理相对复杂建议先跑通非流式再上流式。如果使用云端模型考虑开启模型缓存。Gemini有上下文缓存能把反复发送的历史消息缓存起来大幅降低成本。我用下表总结一下我的调优前后对比优化操作平均单次会话token响应延迟成本影响全量历史约6000约2.5秒高滑动窗口30条约3500约1.8秒中窗口摘要压缩约2500约1.5秒低实测下来滑动窗口加摘要压缩是最划算的组合记忆效果基本不受影响成本和响应速度都有明显改善。6. 补充一些使用后的个人心得我用了Genkit一个月后最大的感受是它把“代理”这件事从手工活变成了织布活。你不需要再自己写身份验证、请求签名、消息转换、工具分发这些基础代码而是可以专注在业务规则、工具设计、Prompt优化这些真正影响体验的地方。多回合代理最难的从来不是“接好大模型”而是“让模型在正确的时间调用正确的工具并且记住自己已经做过的操作”。Genkit的Flow、工具注册、消息管理帮我把这套机制固定了下来。如果你想快速验证思路我建议不要一开始就上Redis、上云端模型先用本地模型加一个简单的内存存储跑通循环再逐步替换成生产组件。多回合代理的调试很依赖可观测性Genkit自带的开发者UI能清楚看到每一次模型请求、每一轮工具调用有了它再复杂的循环问题也能快速定位。最后再分享一个小细节工具描述里的每一个字都值得反复打磨。模型是否知道“什么时候该调用退货政策”很大程度上取决于你描述里的触发条件写得是否准确。我在迭代中发现把业务示例直接写进工具描述可以明显提高调用准确率比如“当用户提到退货、退款、七天无理由时调用getRefundPolicy”这比只写“查询退货政策”效果好得多。如果项目可以持续积累建议用真实对话日志去回放测试不断优化描述这比一味换更大模型更划算。
网站建设高端定制企业官网