这篇文章用 Go 和 OpenAI Chat API 搭建了一个最小 Agent Loop。读懂它之后,你会看到 Agent 的核心并不神秘:模型决定是否调用工具,程序执行工具并把结果放回消息历史,再次请求模型,直到模型给出最终答案或达到步数上限。
要实现的最小闭环
普通模型调用通常只有“输入消息 → 返回文本”。Agent Loop 多出一条行动分支:模型可以返回结构化工具调用,宿主程序执行后把 observation 作为 Tool Message 回填,模型再根据新信息继续推理。
原作者把核心概括为“一个消息循环加一个工具执行器”。工程上可以拆成六个部分:配置加载、模型客户端、工具抽象、工具注册、模型 Schema 转换,以及循环控制。
环境与客户端配置
项目使用 Go,并通过适配器统一不同 OpenAI-compatible 服务商的环境变量。业务层只消费统一的 Config,而环境适配层负责处理诸如 OPENAI_API_KEY、DASHSCOPE_API_KEY、Base URL 和默认地址之间的差异。
这样做的好处是 Agent Loop 不需要知道密钥来自哪家服务。切换供应商时主要修改适配器和模型配置,不必改动工具执行流程。实际使用时,密钥仍应从环境或密钥系统注入,日志中不要输出完整请求头和配置值。
用 ToolBox 统一工具能力
工具接口只要求两件事:返回 Schema,以及根据 JSON 参数执行 Call。Schema 描述名称、用途和参数结构;Call 接收上下文与参数并返回字符串结果或错误。
ToolBox 是工具的注册表和单一入口,负责保存工具、按名称查找以及执行。函数型工具再由 FunctionTool 封装,使业务开发者只需提供名称、说明、参数 Schema 和处理函数,不必为每个小工具重复定义完整类型。
这个结构也给扩展留下空间。MCP、HTTP、远程服务或测试 Mock 只要实现相同接口,就能进入同一套注册和调用流程。调用模型前,再把内部 Tool Schema 转换为模型 SDK 接受的工具参数,避免业务工具直接依赖某一家 SDK。
主循环怎样工作
主程序维护跨用户轮次的 messages,每次输入先追加 User Message,然后进入 runAgentLoop。循环过程可以概括为:
- 使用当前消息历史和工具 Schema 请求模型;
- 把模型返回的 Assistant Message 追加到历史;
- 若没有 Tool Call,返回最终文本;
- 若有 Tool Call,解析工具名和 JSON 参数;
- 通过 ToolBox 执行,每个结果都带对应的 Tool Call ID 回填;
- 带着更新后的消息历史进入下一步。
示例把最大步数设为 20。工具执行失败时,错误会被转换为结构化结果继续回填,让模型有机会判断是否修正参数或更换方案;超过上限则明确报错,防止无限循环。
消息历史能做什么,不能做什么
User Message、Assistant Tool Call 和 Tool Result 持续追加后,下一轮可以看到此前发生的事情,这就是最小上下文记忆。但它不是长期记忆:没有压缩、摘要、检索、向量库或淘汰策略。对话变长后仍需要单独设计上下文预算和敏感信息清理。
运行前的验证清单
先注册一个低风险、可预测的测试工具,确认 Schema 能被模型识别;再测试不存在的工具名、非法 JSON 参数和工具超时;检查每个 Tool Result 是否对应正确的调用 ID;确认无工具调用时能正常退出;最后验证达到最大步数时会停止。
原帖中的 Bash Tool 适合演示,但生产环境还需补充命令白名单、隔离工作目录、超时、输出上限、网络边界和高风险动作确认。最小循环解决的是控制流,安全、恢复、并发、幂等和上下文管理仍属于后续工程化工作。
原作者:diary_O(Polaris_Go)
原帖:https://linux.do/t/topic/2684377