news 2026/10/20 6:48:38

100行代码手写Hermes式智能体框架并接入飞书机器人

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
100行代码手写Hermes式智能体框架并接入飞书机器人

1. 为什么我要自己动手写一个智能体框架

1.1 从“玩具”到“工具”的转折点

市面上开源的智能体框架我几乎都翻过一遍。LangChain 生态庞大,但依赖链太长,一个简单的对话循环要引入几十个包;AutoGen 的多智能体协作很惊艳,可一旦要接入企业内部系统,配置复杂度直线上升。我真正想要的东西其实很朴素:一个能跑在单进程里、依赖极少、逻辑透明、方便接入飞书这类协作平台的轻量级智能体内核。

Hermes 式智能体的核心思路给了我很大启发——它不追求大而全,而是把“感知-决策-执行-反馈”这个循环做得极其干净。所谓 Hermes 式,我理解就是像一个信使一样,在用户意图和实际工具之间做高效、可靠的消息传递与任务编排。它不需要庞大的编排引擎,核心就是一个循环加上一套工具注册机制。

这个项目解决的核心问题是:让一个开发者能在半小时内理解全部代码,并且在一小时内把它接入自己的飞书机器人。适合谁参考?适合那些被重型框架折磨过、想回归本质的中级开发者,也适合刚接触智能体概念、想通过读源码来理解原理的初学者。100 行核心代码不是噱头,是我反复删减后的结果——每一行都承担明确职责,没有一行是为了“看起来专业”而存在的。

1.2 飞书作为接入端的独特价值

选择飞书作为首个接入平台,原因很实际。飞书的开放平台文档清晰,机器人创建流程简单,Webhook 和事件订阅两种模式都支持得很好。更重要的是,飞书的消息卡片交互能力很强,智能体返回的结构化内容可以直接渲染成可点击的卡片,用户体验比纯文本好一个档次。

从技术角度看,飞书的事件回调机制天然适合智能体的异步处理模式。用户发一条消息,飞书服务器推送到我的回调地址,智能体处理后通过 API 回复。这个链路里,我的核心代码只需要关心“收到消息→处理→返回结果”这三步,平台相关的鉴权、签名校验、消息格式转换都可以封装在适配层里。这样一来,核心的智能体逻辑和平台接入逻辑就彻底解耦了,以后想接入其他协作平台,只需要换一个适配器。

注意:飞书机器人有两种消息接收模式——长连接和 Webhook。长连接适合本地开发调试,不需要公网地址;Webhook 适合生产部署,但需要处理签名校验和重试机制。我建议开发阶段用长连接,上线前再切 Webhook。

2. 核心架构设计与关键取舍

2.1 为什么是 100 行——删减的艺术

很多人看到“100 行”第一反应是“肯定功能很弱”。实际上,这 100 行只包含最核心的智能体循环,不包含飞书适配层、不包含具体工具实现、不包含配置文件。如果把所有东西都算上,大概 400 行左右。但核心循环确实只有 100 行,因为我把所有能剥离的东西都剥离了。

具体来说,我删掉了这些东西:内置的对话历史管理(改用外部字典简单存储)、复杂的提示词模板引擎(直接用 f-string)、多模型路由(只保留一个 OpenAI 兼容接口)、向量数据库检索(需要时再手动加)。每删掉一个东西,我都会问自己:没有它,智能体还能完成基本任务吗?答案是能,那就删。

这种做法的好处是调试极其方便。当智能体行为不符合预期时,我不需要在一堆抽象层里找问题,直接看那 100 行循环就能定位。坏处是扩展性需要自己动手,但我觉得这反而是优点——扩展什么、怎么扩展,完全由业务需求决定,而不是被框架的设计绑架。

2.2 核心循环的四个阶段

智能体的核心循环我拆成四个阶段,每个阶段对应一个函数:

  • 感知阶段:接收用户输入,做最基本的清洗和格式化。这里不做复杂的 NLP 处理,因为大模型本身就能理解自然语言,过度预处理反而可能丢失信息。
  • 决策阶段:把用户输入、工具描述、历史上下文拼成一个提示词,发给大模型,让模型决定是直接回答还是调用工具。这里的关键是工具描述的格式要足够清晰,模型才能准确选择。
  • 执行阶段:如果模型返回了工具调用请求,就解析参数、执行对应函数、拿到结果。执行时要加超时和异常捕获,避免一个工具卡死整个循环。
  • 反馈阶段:把工具执行结果再喂给模型,让模型生成最终的自然语言回复。这个阶段可能需要多轮,直到模型不再请求调用工具为止。

这四个阶段用一个 while 循环串起来,设置最大迭代次数防止死循环。实测下来,大部分任务 2-3 轮就能完成,复杂任务也很少超过 5 轮。

2.3 工具注册机制的设计

工具注册我用了最朴素的字典结构,而不是装饰器或者类继承。原因很简单:字典一目了然,新人接手时看一眼就知道有哪些工具、每个工具的参数是什么。

每个工具用一个大模型能理解的描述字符串来定义,包括工具名称、功能说明、参数列表和每个参数的类型与含义。这个描述直接拼进提示词里,模型根据描述来决定调用哪个工具。我试过用 JSON Schema 格式,效果也不错,但字符串描述更灵活,可以加入自然语言的解释。

工具函数的签名我统一成def tool_name(params: dict) -> str,参数从字典里取,返回值统一是字符串。这样做的好处是执行器不需要关心每个工具的具体签名,统一调用即可。坏处是类型检查弱,但智能体场景下,参数正确性由模型保证,人工检查反而多余。

提示:工具描述的质量直接决定模型调用的准确率。我踩过的坑是描述太简短,模型经常选错工具。后来我把每个工具的描述写成一段完整的话,包括“什么时候该用”和“什么时候不该用”,准确率明显提升。

3. 飞书接入的完整实操流程

3.1 创建飞书机器人应用

第一步是在飞书开放平台创建应用。进入开发者后台,选择“创建企业自建应用”,填写应用名称和描述。创建完成后,在“凭证与基础信息”页面拿到 App ID 和 App Secret,这两个后面要用。

接下来配置权限。智能体需要接收消息和发送消息,所以至少要开通im:message、im:message:send_as_bot这两个权限。如果要用消息卡片,还需要im:message:card相关权限。权限开通后需要发布版本才能生效,企业内部应用一般审核很快。

然后配置事件订阅。在“事件订阅”页面,选择“使用长连接接收事件”,这样本地开发时不需要公网地址。添加im.message.receive_v1事件,当用户给机器人发消息时,飞书会通过长连接推送过来。

最后在“机器人”页面启用机器人功能,设置机器人名称和头像。到这里,飞书侧的配置就完成了。

3.2 长连接模式下的消息收发

长连接模式我用飞书官方提供的 SDK 来实现。核心逻辑是:SDK 建立连接后,注册一个消息处理函数,每当收到消息事件,就调用智能体的处理入口,拿到回复后再通过 SDK 发送回去。

这里有个细节需要注意:飞书的消息事件里,消息内容可能是纯文本、富文本或者图片。我的处理方式是只取纯文本部分,其他类型统一回复“暂不支持该消息类型”。这样做虽然功能有限,但保证了核心链路的稳定性。后续要扩展富文本处理,只需要在适配层加解析逻辑,不影响智能体内核。

发送消息时,我用了回复消息的接口,而不是主动发消息。回复消息会自动带上引用关系,用户在飞书里能看到上下文,体验更好。消息格式我优先用交互卡片,把智能体的回复包装成卡片内容,支持 Markdown 渲染。如果卡片发送失败,降级为纯文本发送。

3.3 消息去重与并发处理

飞书的事件推送可能会重复,比如网络抖动导致飞书没收到确认,就会重推。如果不做去重,用户会收到重复回复。我的做法是用消息 ID 做去重,维护一个最近处理过的消息 ID 集合,处理前先检查是否已处理过。

并发处理方面,长连接模式下 SDK 是单线程回调,如果智能体处理时间较长,会阻塞后续消息。我的解决方案是把消息处理放到线程池里,回调函数只负责把消息丢进队列就返回。线程池大小设为 4,实测足够应对个人使用场景。如果多人同时使用,可以适当调大,但要注意大模型 API 的速率限制。

注意:线程池里的任务如果抛异常,一定要捕获并记录日志,否则异常会静默丢失,排查起来很痛苦。我在每个任务外层都包了 try-except,把异常信息写到日志文件里。

4. 智能体内核的逐行拆解

4.1 提示词构造的关键细节

提示词是智能体的“大脑说明书”,构造得好不好直接决定效果。我的提示词分三部分:系统角色描述、工具列表、对话历史。

系统角色描述我写得很具体,不是简单的“你是一个助手”,而是“你是一个任务型智能体,可以调用工具来帮助用户完成任务。当用户的问题需要实时信息或外部操作时,优先调用工具;当用户只是闲聊或问常识时,直接回答”。这样模型能清楚知道自己的定位。

工具列表的格式我反复调整过。最终版本是每个工具一段,包含工具名、功能描述、参数说明。参数说明里,我明确写了每个参数的类型和是否必填。比如“参数:city(字符串,必填),表示城市名称”。模型对“必填”这个词很敏感,写了之后漏参数的情况明显减少。

对话历史我只保留最近 10 轮,太早的历史对当前决策帮助不大,反而增加 token 消耗。历史格式是“用户:xxx”和“助手:xxx”交替,工具调用结果也作为助手消息的一部分记录进去。

4.2 工具调用的解析与执行

模型返回的工具调用请求,格式我约定为一段特定标记包裹的 JSON。比如模型输出[TOOL_CALL]{"name": "get_weather", "params": {"city": "北京"}}[/TOOL_CALL],我用正则提取出 JSON 部分,解析后执行对应工具。

这里有个坑:模型有时会在 JSON 前后加额外的解释文字,比如“好的,我来调用工具:[TOOL_CALL]...”。我的正则做了兼容,只要标记存在就能提取出来。如果解析失败,就把模型的原始输出当作最终回复返回,避免卡死。

执行工具时,我加了 10 秒超时。超时后返回“工具执行超时”的错误信息给模型,让模型决定是重试还是换一种方式回答。异常捕获也是必须的,任何工具抛异常,都捕获后把异常信息返回给模型,模型通常会根据错误信息调整策略。

4.3 多轮循环的终止条件

循环终止有两个条件:一是模型不再请求调用工具,直接返回了文本回复;二是达到最大迭代次数(我设为 8)。达到最大次数时,我会把当前所有信息汇总,让模型生成一个总结性回复,而不是直接报错。

实测中,大部分任务 2-3 轮就结束了。需要多轮的一般是复杂查询,比如“帮我查一下明天北京的天气,如果下雨就提醒我带伞,顺便看看有没有合适的室内活动”。这个任务需要先查天气,再根据天气结果决定是否查活动,至少 3 轮。

最大迭代次数设为 8 是经验值。设太小,复杂任务完不成;设太大,万一模型陷入循环会浪费大量 token。8 轮足够覆盖绝大多数场景,同时 token 消耗可控。

5. 常见问题与排查技巧实录

5.1 模型不调用工具怎么办

这是最常见的问题。模型直接用自己的知识回答,而不是调用工具。原因通常有三个:工具描述不够清晰、系统提示词没有强调工具优先级、模型本身能力不足。

排查步骤:先看工具描述,是不是太简短或者有歧义。我遇到过“查询天气”这个描述,模型以为是自己知道天气,改成“调用外部天气 API 获取指定城市的实时天气”后就正常了。然后看系统提示词,有没有明确说“需要实时信息时优先调用工具”。最后换一个能力更强的模型试试,有些小模型对工具调用的支持确实弱。

5.2 工具参数解析失败

模型返回的 JSON 格式不对,比如用了单引号、少了括号、参数名拼错。我的处理方式是:先尝试标准 JSON 解析,失败后用正则做宽松提取,再失败就把原始输出返回给模型,让模型重新生成。

预防措施是在提示词里明确 JSON 格式要求,并给一个示例。示例的作用很大,模型会模仿示例的格式。我给的示例是{"name": "工具名", "params": {"参数名": "参数值"}},简单清晰。

5.3 飞书消息发送失败

常见原因有:token 过期、权限不足、消息格式错误。token 过期最常见,飞书的 tenant_access_token 有效期 2 小时,需要定时刷新。我的做法是每次发送前检查 token 是否过期,过期就重新获取。

权限不足一般是权限没开通或者版本没发布。消息格式错误通常是卡片 JSON 写错了,飞书对卡片格式要求较严,建议先用飞书的卡片搭建工具验证格式。

问题现象可能原因排查方法解决方案
模型不调用工具描述不清/提示词弱检查工具描述和系统提示细化描述,强调工具优先级
参数解析失败JSON 格式错误打印模型原始输出加示例,宽松解析,失败重试
消息重复回复事件重推检查消息 ID 去重逻辑维护已处理消息 ID 集合
token 过期未定时刷新检查 token 获取时间每次发送前检查并刷新
工具执行超时外部 API 慢加日志记录耗时设超时,返回错误让模型决策

5.4 性能优化的几个实用技巧

第一,工具描述和系统提示词可以缓存,不需要每次请求都重新构造。我用了一个简单的字典缓存,key 是工具集合的哈希,value 是拼好的提示词。

第二,对话历史不要无限增长。我设了 10 轮上限,超出后丢弃最早的。如果发现模型经常需要更早的上下文,可以考虑做摘要压缩,但那是另一个话题了。

第三,大模型 API 调用是瓶颈,能并行就并行。比如多个独立工具调用,可以同时发起。但要注意速率限制,我一般控制在并发 3 以内。

第四,日志要详细但不要冗余。我记录每次请求的输入、输出、耗时、token 消耗,但不记录完整的提示词(太长)。排查问题时,这些日志足够定位大部分问题。

6. 扩展方向与个人体会

6.1 从单智能体到多智能体协作

当前是单智能体架构,所有工具注册在一个列表里。如果工具数量超过 20 个,模型选择工具的准确率会下降。这时候可以考虑拆分:按领域分成多个子智能体,每个子智能体只负责一类工具,主智能体负责路由。

比如一个“办公助手”主智能体,下面分“日程管理”“文档处理”“消息通知”三个子智能体。用户请求先到主智能体,主智能体判断属于哪个领域,转发给对应子智能体。子智能体处理完把结果返回主智能体,主智能体再回复用户。

这种架构的复杂度会上升,但工具选择的准确率会明显提升。我的建议是:工具少于 15 个时用单智能体,超过 20 个再考虑拆分。

6.2 记忆系统的轻量级实现

当前对话历史只存在内存里,重启就丢失。要持久化的话,最简单的方案是用 SQLite 存对话记录,按用户 ID 和会话 ID 索引。每次处理前从数据库加载最近 10 轮,处理后写入新记录。

更进一步的记忆系统可以加入“长期记忆”:把重要的用户偏好、常用信息提取出来单独存储,每次对话时作为背景信息注入提示词。比如用户说过“我住在北京”,这个信息就存进长期记忆,以后查天气时默认城市就是北京。

长期记忆的提取可以用大模型来做,让模型判断哪些信息值得记住。但要注意隐私,敏感信息不要存。

6.3 我在这个项目里踩过的坑

最大的坑是低估了飞书事件推送的重复率。上线第一天就发现用户收到重复回复,排查后发现是飞书在没收到确认时会重推。加了消息 ID 去重后解决。

第二个坑是工具执行没有超时控制。有个工具调用的外部 API 挂了,请求一直不返回,整个智能体卡死。后来加了 10 秒超时,超时后返回错误信息,模型会尝试其他方式回答。

第三个坑是提示词太长导致 token 消耗过大。最初我把所有工具描述和历史都拼进去,一次请求 4000 多 token。后来精简了工具描述,历史限制 10 轮,降到 1500 token 左右,成本降了一半多。

这个项目后续还可以这样扩展:接入更多协作平台,把适配层做成插件式;加入定时任务能力,让智能体可以主动推送消息;支持图片和文件处理,扩展多模态能力。但核心的 100 行循环我打算保持稳定,所有扩展都通过外围模块实现,确保内核始终清晰可控。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 6:48:14

Kafka分区堆积根因:Coordinator元数据同步失败

1. 问题现场还原:不是“消息积压”,而是“局部失血性休克”看到标题里“kafka部分partition消息堆积”,很多人第一反应是“消费者太慢”“吞吐不够”“该扩容了”。我去年在某跨平台系统做稳定性加固时,也这么想——直到连续三天凌…

作者头像 李华
网站建设 2026/10/20 6:47:35

PPO与GRPO优势估计对比:为什么GRPO能省掉Critic网络

1. 从一道面试题说起:为什么“砍掉Critic”反而成了加分项如果你最近在准备算法岗面试,尤其是大模型对齐、强化学习方向,大概率会被问到这样一个问题:“PPO里的Critic网络到底在干什么?如果把它省掉,优势函…

作者头像 李华
网站建设 2026/10/20 6:47:19

vitalsource-dl实战:将VitalSource已授权电子书抓取为离线PDF/EPUB指南

简介:vitalsource-dl 是一份基于 Node.js 开发的 VitalSource 电子书下载器,面向需要将正版已购教材转存为本地 EPUB 的读者,以及想学习 Node.js 下载脚本写法的开发者。针对在线阅读不便备份、离线阅读受限的问题,脚本通过读取浏…

作者头像 李华
网站建设 2026/10/20 6:47:18

PMSM低速大负载启动难?IF控制策略Matlab/Simulink仿真实战解析

一个老问题——永磁同步电机(PMSM)低速大负载启动怎么搞。我自己做无位置传感器控制时,最纠结的环节就是零低速段的转子初始位置辨识和可靠启动。试过高频注入、试过磁极辨识,折腾一圈回到最务实的方案,就是IF控制&…

作者头像 李华
网站建设 2026/10/20 6:47:02

Artcraft:融合手艺与艺术的系统化创作新范式

1. “Artcraft”不是新词,而是被长期低估的创作范式重启信号最近在多个设计社区、开源项目讨论区和创意工作坊里,“artcraft”这个词频繁跳出来——不是作为某个具体工具或平台的名字,而是像一个突然被擦亮的老铜牌,上面刻着“手艺…

作者头像 李华
网站建设 2026/10/20 6:47:01

智能体技能模块化设计:可测试、可插拔、可治理的行为引擎

1. 项目概述:这不是一个“技能库”,而是一套可插拔的智能体行为引擎“agent-skills”这个名称乍看像某个开源工具包的名字,但实际在当前技术演进脉络里,它代表的是一种范式转移——从“写死逻辑”的自动化脚本,转向“按…

作者头像 李华