资讯动态

从零手写智能体:100行核心代码接入飞书,实现工具调用与Agent Loop

发布时间:2026/10/20 22:09:46 来源:尧图企业网站定制
1. 为什么我要自己动手写一个智能体1.1 从玩具到工具的那道坎市面上开源的智能体框架我几乎都折腾过一遍。说实话能跑通 Demo 的一抓一大把但真正能扔到日常工作流里、让同事无感使用的少之又少。大部分框架的问题在于要么把逻辑写死在代码里改一个行为就得翻半天源码要么接入聊天平台这一步极其繁琐光是一个回调验证就能卡住半天。我当时的诉求很朴素团队日常沟通都在飞书上我希望有一个机器人能听懂自然语言指令自己决定调用哪些工具、按什么顺序执行最后把结果整理好发回群里。听起来简单但真做起来你会发现让模型自己规划步骤这件事才是智能体和普通问答机器人的分水岭。Hermes 式智能体的核心思路就是让模型扮演一个调度中枢的角色。它不直接回答问题而是先判断这个问题需要拆成几步每一步该用哪个工具工具返回的结果够不够不够的话下一步该干什么这个循环往复的过程就是所谓的 Agent Loop。我把它压缩到 100 行核心代码不是炫技而是想证明一件事智能体的骨架其实很轻重的是工程细节。1.2 这个智能体到底能干什么具体来说我做的这个东西能接入飞书群聊接收 消息后自动判断意图。比如你说帮我查一下今天服务器告警情况顺便看看昨天的部署记录它会先调用监控查询工具拿到告警列表再调用部署记录工具拉取历史最后把两段信息合并成一条结构化的回复发回群里。它适合谁参考我觉得有三类人一是想理解智能体底层运转逻辑的开发者二是需要快速给团队搭一个内部助手的运维或研发三是想拿一个轻量骨架做二次开发的人。你不需要懂什么高深的强化学习只要会写 Python 函数、能看懂 JSON就能把这套东西跑起来。提示本文所有代码和配置均为示意性伪代码重点在于讲清楚设计思路和关键环节实际落地时需要根据你的环境调整。2. 整体架构设计与核心思路拆解2.1 为什么选择函数即工具的极简模式很多框架喜欢搞一套复杂的工具注册机制什么装饰器、什么插件市场看起来很唬人。但我实测下来最稳、最好维护的方式反而是最土的一个工具就是一个普通的 Python 函数函数名就是工具名docstring 就是工具描述参数类型注解就是入参 schema。为什么这么设计因为大模型理解工具的方式本质上就是读一段自然语言描述。你写def query_alerts(service_name: str) - list:模型看到的就是这是一个查询告警的函数需要一个服务名参数。这比任何花哨的注册机制都直观。而且这样做有个巨大的好处你新增一个工具只需要写一个函数不用改任何框架代码。我踩过的坑是早期我用了一个流行的框架它的工具注册需要继承一个基类还要实现好几个抽象方法。结果我想加一个简单的查天气功能写了 80 行样板代码。后来我彻底放弃改成函数即工具同样的功能 5 行搞定。这个取舍背后的逻辑是智能体的复杂度应该花在决策逻辑上而不是工具接入上。2.2 100 行核心代码的边界在哪里我得先泼一盆冷水100 行指的是核心的 Agent Loop 和工具调度逻辑不包括飞书 SDK 的接入、HTTP 服务的搭建、配置文件的读取这些外围代码。如果你把这些全算上大概在 300 到 400 行左右。但核心的智能部分确实可以压缩到 100 行以内。这 100 行干了什么事简单说就是四件事接收用户消息、构造提示词让模型决策、解析模型的工具调用请求、执行工具并把结果喂回模型。循环这个过程直到模型认为可以给出最终答案为止。听起来简单但每一步都有讲究后面我会逐个拆解。架构上我分了三层接入层负责和飞书通信处理消息的接收与发送决策层就是那 100 行核心负责和模型交互、调度工具工具层是一堆独立的函数每个函数干一件具体的事。三层之间通过标准的数据结构通信互不耦合。这样做的好处是哪天你想把飞书换成别的平台只需要改接入层决策层和工具层原封不动。2.3 消息流转的完整链路一条消息从用户发出到收到回复中间经历了什么我用一个实际场景来说明。假设用户在飞书群里 机器人说帮我看看订单服务今天的错误率如果超过 5% 就拉个告警。第一步飞书通过 Webhook 把这条消息推送到我的服务端。服务端先做签名验证确认消息来源合法然后提取出消息文本和发送者信息。第二步把消息文本和系统提示词一起发给模型。系统提示词里写清楚了你是一个运维助手你可以调用以下工具请根据用户需求决定调用哪些工具。第三步模型返回一个结构化的响应里面包含它想调用的工具名和参数比如query_error_rate(serviceorder, datetoday)。第四步我的代码解析这个响应找到对应的函数并执行拿到结果。第五步把工具结果追加到对话历史里再次发给模型。模型看到错误率是 7%超过阈值于是决定调用create_alert(serviceorder, threshold5)。第六步执行告警创建把结果再喂回模型。这次模型认为任务完成返回最终的自然语言总结。第七步我把这个总结通过飞书 API 发回群里。整个链路里最关键的其实是第三步和第五步之间的循环控制。你得设置一个最大循环次数防止模型陷入死循环还得处理工具执行失败的情况把错误信息也喂回模型让它自己决定是重试还是换方案。3. 核心细节解析与实操要点3.1 系统提示词怎么写才管用系统提示词是智能体的大脑说明书写得好不好直接决定它聪不聪明。我试过很多版本最后总结出一个结构角色定义 工具清单 行为约束 输出格式。角色定义要具体别写你是一个有用的助手要写你是一个运维智能体负责查询监控数据、分析告警、执行简单的运维操作。工具清单不用你手写可以用代码自动从函数签名和 docstring 生成这样新增工具时提示词自动更新不会遗漏。行为约束是最容易被忽略的部分我通常会写三条一是如果用户请求不明确先追问再执行二是每次只调用一个工具等结果返回后再决定下一步三是如果工具返回错误尝试换一种方式或告知用户。输出格式这块我强制模型用 JSON 返回包含thought、action、action_input三个字段。thought是模型的思考过程方便调试action是要调用的工具名如果是最终回答就填final_answeraction_input是工具参数或最终答案。为什么要强制 JSON因为自然语言解析太容易出错了模型稍微换个说法你的正则就匹配不上。JSON 虽然死板但稳定。注意不同模型对 JSON 格式的遵循程度差异很大。我实测下来参数量大一些的模型在格式遵循上明显更稳。如果你用的是小模型可能需要在提示词里多给几个示例或者用 JSON Schema 做约束。3.2 工具函数的三个设计原则写工具函数看着简单但要让模型用得顺手得遵守三个原则。第一函数名要自解释。别用qry_alt这种缩写要用query_alerts。模型是靠名字和描述来匹配意图的名字越清晰匹配越准。我见过有人把工具命名成tool_1、tool_2结果模型根本不知道该调哪个。第二参数要少而精。一个工具最好只接受 1 到 3 个参数而且每个参数都要有明确的类型注解和描述。参数太多模型容易填错参数太少功能又不够用。如果某个工具确实需要很多参数考虑拆成多个工具或者把不常用的参数设成有默认值的可选参数。第三返回值要结构化。别返回一大段自然语言要返回 JSON 或者列表。模型处理结构化数据的能力远强于处理长文本。比如查询告警返回[{service: order, level: critical, count: 3}]就比返回订单服务有 3 条严重告警要好因为前者模型可以直接做统计和比较。我踩过的一个坑是早期我有个工具返回了带换行和特殊字符的长字符串结果模型在解析时经常出错。后来我统一改成返回 JSON 字符串问题就消失了。这个经验告诉我工具和模型之间的接口越规范越好。3.3 循环控制与异常处理Agent Loop 最容易出问题的地方就是循环控制。如果不加限制模型可能反复调用同一个工具或者在一个死胡同里打转。我的做法是设置两个阈值最大循环次数和最大连续相同工具调用次数。最大循环次数我一般设 10 次。超过 10 次还没得出最终答案说明要么任务太复杂要么模型卡住了这时候直接返回一个兜底回复比如任务处理超时请简化您的请求。最大连续相同工具调用次数设 3 次如果模型连续 3 次调用同一个工具且参数相同说明它没理解工具返回的结果这时候我会在对话历史里插入一条系统提示你已经调用过这个工具了结果是 XXX请基于这个结果继续。异常处理分两种工具执行异常和模型调用异常。工具执行异常时我把错误信息包装成工具结果喂回模型让它自己决定怎么办。模型调用异常比如超时、限流时我会做指数退避重试重试 3 次还失败就放弃。这里有个细节重试时不要把失败的请求也加到对话历史里否则会污染上下文。4. 实操过程与核心环节实现4.1 环境准备与依赖安装先把基础环境搭起来。我用的是 Python 3.10因为这个版本的类型注解支持比较完善。依赖就三个requests用来发 HTTP 请求flask用来起 Web 服务接收飞书回调openai或者对应的模型 SDK 用来调模型。如果你用的是别的模型服务把 SDK 换掉就行核心逻辑不变。pip install requests flask openai飞书这边你需要创建一个企业自建应用拿到app_id和app_secret然后在权限管理里开通接收消息和发送消息的权限。事件订阅里配置好你的服务端地址飞书会往这个地址推送消息事件。这里有个坑飞书的事件推送需要你先完成 URL 验证它会发一个带challenge参数的请求你要原样返回challenge值才算验证通过。# 飞书 URL 验证的示意代码 app.route(/webhook, methods[POST]) def webhook(): data request.json if challenge in data: return {challenge: data[challenge]} # 处理正常消息 handle_message(data) return {code: 0}4.2 核心 Agent Loop 的实现这是整个项目的灵魂我把它拆成几个关键步骤来讲。首先是构造初始消息列表包含系统提示词和用户消息。然后进入循环每次循环做四件事调模型、解析响应、执行工具、追加结果。def agent_loop(user_message, max_turns10): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_message} ] for turn in range(max_turns): response call_model(messages) parsed parse_response(response) if parsed[action] final_answer: return parsed[action_input] tool_result execute_tool(parsed[action], parsed[action_input]) messages.append({role: assistant, content: response}) messages.append({role: user, content: f工具结果{tool_result}}) return 任务处理超时请简化您的请求这段代码看着简单但有几个细节值得说。第一parse_response要做容错模型有时候会在 JSON 外面包一层 markdown 代码块标记你得把它剥掉。第二execute_tool要做参数校验模型可能传错参数类型比如该传整数的地方传了字符串你得做转换或者返回错误提示。第三追加工具结果时我用的是role: user因为大多数模型 API 不支持role: tool用 user 角色兼容性最好。4.3 工具注册与自动发现工具注册我用了一个很取巧的办法约定大于配置。所有工具函数放在一个tools.py文件里函数名以tool_开头。启动时扫描这个文件把所有符合条件的函数收集起来自动生成工具清单和提示词。import inspect import tools def discover_tools(): tool_list [] for name, func in inspect.getmembers(tools, inspect.isfunction): if name.startswith(tool_): sig inspect.signature(func) params {p.name: p.annotation.__name__ for p in sig.parameters.values()} tool_list.append({ name: name, description: func.__doc__, parameters: params }) return tool_list这样做的好处是新增工具时你只需要在tools.py里写一个函数重启服务就自动生效不用改任何配置。我实测下来这个机制让工具扩展的效率提升了至少三倍。唯一需要注意的是函数名和 docstring 要写清楚因为模型就是靠这两样东西来理解工具的。4.4 飞书消息的接收与回复飞书消息的接收分两种模式Webhook 推送和长连接。Webhook 需要你的服务端有公网地址长连接则不需要但实现起来稍微复杂一点。我选的是 Webhook因为调试方便能看到完整的请求日志。接收消息后先做签名验证然后提取消息内容。飞书的消息体结构比较复杂文本消息在event.message.content里是个 JSON 字符串需要二次解析。提取到文本后去掉 机器人的部分剩下的就是用户的实际指令。回复消息时我建议用回复消息接口而不是发送消息接口因为回复消息会自动带上引用关系群里看起来更清晰。回复的内容如果比较长可以拆成多条或者用飞书的富文本卡片格式展示效果更好。我一般会把最终答案用卡片发出去卡片里包含标题、正文和几个关键指标看起来专业很多。提示飞书的消息发送有频率限制短时间内发太多会被限流。如果你的智能体需要频繁回复建议加一个发送队列控制发送速率。5. 常见问题与排查技巧实录5.1 模型不按格式返回怎么办这是最常见的问题尤其是用一些小模型或者国产模型时。模型可能返回一段自然语言而不是你要求的 JSON。我的排查思路是先看提示词里有没有给足够的格式示例如果只说了请用 JSON 返回但没给例子模型很容易跑偏。加上两三个完整的示例后遵循率会大幅提升。如果加了示例还是不行那就得在代码层面做兜底。我写了一个extract_json函数用正则从响应里提取第一个完整的 JSON 对象。即使模型在 JSON 前后加了一堆废话也能提取出来。再不行就做一次重试在重试的提示词里强调上次你的返回格式不对请严格按照 JSON 格式返回。还有一个技巧是降低温度参数。温度越高模型越有创造力但格式遵循越差。我把温度设在 0.1 到 0.3 之间格式稳定性明显好转。这个参数没有绝对标准你得根据自己用的模型多试几次。5.2 工具调用参数错误的处理模型填错参数是家常便饭。常见错误有三类参数名写错、参数类型不对、缺少必填参数。我的处理策略是在execute_tool里做严格的参数校验任何一项不通过就返回一个结构化的错误信息告诉模型哪里错了、应该怎么改。def execute_tool(name, params): func TOOL_MAP.get(name) if not func: return {error: f工具 {name} 不存在可用工具{list(TOOL_MAP.keys())}} sig inspect.signature(func) try: sig.bind(**params) except TypeError as e: return {error: f参数错误{str(e)}请检查参数名和类型} try: return func(**params) except Exception as e: return {error: f工具执行失败{str(e)}}这个错误信息的设计很关键它要足够具体让模型能自己纠正。比如告诉它可用工具列表它下次就不会再调不存在的工具了。我实测下来加上这个机制后参数错误导致的失败率下降了大概 70%。5.3 循环卡死的排查与预防循环卡死通常有两种表现模型反复调用同一个工具和模型一直不给出最终答案。排查时我一般先看日志把每一轮的thought和action打出来看看模型在想什么。如果发现它反复调同一个工具说明它没理解工具返回的结果这时候我会在提示词里加一句请仔细阅读工具返回的结果不要重复调用同一个工具。预防措施有三条一是设置最大循环次数这个前面说过了二是设置连续相同调用检测连续 3 次相同就强制干预三是在工具返回结果里加上明确的下一步建议比如工具返回{result: [...], hint: 数据已获取请基于此结果给出最终答案}。这个 hint 字段对模型很有引导作用能显著减少无意义的循环。5.4 常见问题速查表问题现象可能原因排查方法解决方案模型返回非 JSON提示词示例不足检查提示词增加格式示例降低温度工具调用参数错误参数描述不清查看工具 docstring完善参数类型注解和描述循环卡死模型未理解工具结果打印每轮 thought加 hint 字段设循环上限飞书消息收不到签名验证失败查看服务端日志检查 app_secret 和签名算法回复消息乱码编码问题检查 content 解析统一用 UTF-8 编码响应速度慢模型调用耗时统计各环节耗时换更快的模型或加缓存5.5 几个让我少走弯路的实操心得第一个心得日志一定要打全。我早期为了省事只打了最终结果结果出问题时完全不知道中间发生了什么。后来我把每一轮的模型输入、输出、工具调用、工具结果全部打到日志里排查效率提升了不止一个档次。日志级别用 DEBUG生产环境可以关掉但开发阶段千万别省。第二个心得工具要幂等。什么意思就是同一个工具用同样的参数调用多次结果应该是一样的。因为模型可能会重复调用如果你的工具是创建告警这种有副作用的操作重复调用就会创建多条告警。我的做法是给有副作用的工具加一个去重机制比如用参数生成一个唯一 ID短时间内相同 ID 的调用直接返回缓存结果。第三个心得先跑通再优化。我见过太多人一上来就追求完美架构结果卡在某个细节上迟迟跑不起来。我的建议是先用最土的办法把整个链路跑通哪怕工具只有一个、提示词写得很烂只要能看到用户发消息、模型调工具、结果发回去这个完整流程后面优化就有方向了。我第一版只用了 40 行代码工具就一个查时间但跑通那一刻的成就感比什么都强。第四个心得给模型留退路。当模型实在无法完成任务时要有一个兜底的回复而不是让用户干等。我的兜底策略是如果循环超过上限就返回抱歉我暂时无法处理这个请求请尝试换一种说法或联系人工。这句话看着简单但能避免很多用户困惑。6. 后续扩展与个人体会这套东西跑通之后我陆续加了不少工具进去查日志的、查配置的、发通知的、甚至还有一个能查食堂菜单的。每加一个工具只需要写一个函数其他什么都不用改。这种扩展性是我最满意的地方。如果你也想动手做我的建议是从一个最小的场景开始。别一上来就想做一个全能助手先做一个只能查天气的机器人把整个链路跑通然后再慢慢加功能。智能体的能力上限其实取决于你给它接了多少工具、工具设计得好不好而不是框架有多复杂。我个人在实际操作中的体会是智能体最难的部分不是技术而是对任务的理解和拆解。你得先想清楚用户会怎么提问、模型需要哪些信息才能决策、工具返回什么格式最利于模型理解。这些想明白了代码写起来很快。想不明白写再多代码也是白搭。最后分享一个小技巧如果你想让智能体更聪明可以在系统提示词里加一段思考示例展示一个完整的任务拆解过程。模型看到示例后会模仿这种思考方式决策质量会有明显提升。这个技巧我用了之后工具调用的准确率大概提升了 20% 左右而且几乎不增加任何代码复杂度。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑