☰
100行代码复刻Hermes式智能体:飞书机器人从问答到自主循环的工程实践
2026/10/20 14:26:31 网站建设 项目流程

1. 从"消息机器人"到"会思考的同事":我为什么要折腾这件事

飞书群里每天都有大量重复提问:这个接口怎么调、那个字段什么含义、上周的排期文档在哪。一开始我写了个关键词匹配的机器人,命中就回固定话术。用了不到两周就废了——用户换个说法它就懵,问"这个报错咋整"和"接口报错了怎么办"它当成两个问题。这种"if-else 式"的机器人本质上是个高级自动回复,离"智能体"差着十万八千里。

后来我接触到 Hermes 这类智能体的设计思路,核心启发是:智能体不是"问答对"的集合,而是一个能自己决定"下一步做什么"的循环。它拿到用户消息后,会先判断意图,再决定是直接回答、调用工具查资料,还是追问澄清,拿到结果后再判断是否要继续。这个"判断—行动—观察—再判断"的循环,才是智能体和普通机器人的分水岭。

我决定用尽量少的核心代码复刻这个思路,并且直接接入飞书——毕竟团队日常就在飞书里,工具要长在大家顺手的地方才有价值。最终核心逻辑压到了 100 行左右,剩下的都是配置和适配层。这篇文章我会把整个设计思路、关键取舍、踩过的坑全部摊开讲,适合有一定 Python 基础、想给自己的团队做一个"能干活"的智能体、又不想引入重型框架的开发者。读完你能拿到一套可直接复现的最小骨架,以及我在真实使用中总结的一堆经验。

2. Hermes 式智能体的核心:一个"判断—行动"循环到底长什么样

2.1 为什么不是"一问一答",而是"循环"

普通机器人的执行路径是线性的:收到消息 → 匹配规则 → 返回结果。这条路径没有"回头路",一旦匹配错了就没救。而 Hermes 式智能体的执行路径是一个 while 循环,每一轮都做三件事:让模型决定下一步动作、执行这个动作、把结果塞回上下文。循环的终止条件是模型明确表示"我说完了"。

这个设计的好处在于,智能体可以自己"多走几步"。比如用户问"帮我查一下上周那个接口文档,顺便看看有没有更新",它可以先调用搜索工具找到文档,再调用一个"读取文档"的工具,最后综合两份结果回答。整个过程不需要我预先写死"先搜索再读取"的流程,是模型自己根据任务拆解出来的。

我用一个生活化的类比:普通机器人像自动售货机,投币选号出货,路径固定;Hermes 式智能体像便利店店员,你说"我想买点能提神的东西",他会先想"咖啡还是功能饮料",再走到货架,发现咖啡卖完了,回来告诉你"咖啡没了,功能饮料行吗"。这个"走一趟再回来"的能力,就是循环带来的。

2.2 循环里最关键的三个角色

拆开看,这个循环里有三个必须想清楚的角色。

第一个是"决策者",也就是大模型。它负责读当前上下文,输出一个结构化的"下一步动作"。这里有个关键取舍:动作必须是结构化的,不能是自由文本。我一开始让模型直接输出自然语言,结果解析起来极其痛苦,它今天说"我要搜索一下",明天说"让我查查资料",正则写到崩溃。后来改成强制 JSON 输出,格式固定为{"action": "...", "params": {...}},解析瞬间清爽。

第二个是"工具箱",也就是智能体能调用的所有能力。每个工具要有清晰的名字、描述和参数定义。这里最容易踩的坑是工具描述写得太随意,模型根本不知道该在什么时候用。我后面会专门讲工具描述怎么写。

第三个是"记忆",也就是上下文管理。每一轮的动作和结果都要追加到对话历史里,否则模型会"失忆",反复做同一件事。但上下文又不能无限增长,需要有个截断或摘要策略。

2.3 100 行核心代码的边界在哪里

标题说"100 行核心代码",我得诚实交代这 100 行到底包含什么、不包含什么。核心循环、动作解析、工具调度、上下文拼接,这些加起来大概 100 行。但飞书的消息收发、签名校验、事件订阅、HTTP 服务这些适配代码不算在内,工具的具体实现(比如真的去查数据库)也不算在内。

这么切分是有意的。核心逻辑应该和平台解耦,飞书只是其中一个入口,明天要接别的平台,核心那 100 行一行都不用改。这也是我坚持不引入重型框架的原因——框架会把核心逻辑和平台适配揉在一起,看起来省事,实际上想换平台时痛不欲生。

3. 把飞书接进来:消息通道里那些文档不会明说的细节

3.1 事件订阅模式的选择:长连接还是 Webhook

飞书开放平台提供两种接收消息的方式:一种是配置回调地址(Webhook),飞书把消息 POST 到你的服务器;另一种是长连接模式,你的程序主动和飞书建立连接。这两种我都试过,说说真实感受。

Webhook 模式要求你的服务有一个公网可访问的地址,本地开发时得用内网穿透工具把本地端口暴露出去。好处是稳定、可控,适合正式部署。长连接模式不需要公网地址,本地跑起来就能收到消息,调试体验极好,但连接断了要自己处理重连。

我的建议是:开发阶段用长连接快速验证,上线时切到 Webhook。核心逻辑不变,只是入口层换个实现。这里有个细节,长连接模式下事件是推送过来的,你要保证处理逻辑不阻塞接收线程,否则消息会堆积。我的做法是收到事件后立刻丢进一个队列,由单独的 worker 消费。

3.2 消息去重:为什么你的机器人会重复回复

这是我踩的第一个大坑。上线第一天,用户发一条消息,机器人回了三条一模一样的。排查了半天,原因是飞书的事件推送有重试机制——如果你的服务在超时时间内没返回成功响应,飞书会认为投递失败,重新推送。而我的处理逻辑耗时较长(要调模型),超过了超时阈值,于是同一条消息被推了三次,机器人就回了三次。

解决办法有两层。第一层是尽快返回响应,收到事件后先回 200,再异步处理业务逻辑。第二层是做幂等去重,用消息的事件 ID 做键,处理过的 ID 存起来(内存或 Redis 都行),重复的直接丢弃。这两层缺一不可,只做异步不做去重,遇到网络抖动还是会重复;只做去重不做异步,超时问题依然存在。

提示:事件 ID 一定要用飞书给的原始 ID,不要自己用消息内容做哈希。因为用户完全可能连发两条一模一样的消息,那是两次真实请求,不该被去重。

3.3 回复消息的两种姿势:被动回复与主动推送

飞书里回复消息有两种方式。一种是在事件响应里直接带上回复内容,这叫被动回复;另一种是拿到会话 ID 后,主动调用发消息接口推送,这叫主动推送。

被动回复简单,但有时间限制,而且一条事件只能回复一次。主动推送灵活,可以分多条发、可以延迟发,适合智能体这种"思考需要时间"的场景。我的做法是:收到消息后先被动回复一句"收到,正在处理",然后用主动推送把真正的结果发出去。这样用户不会觉得机器人卡住了,体验好很多。

这里还有个细节,主动推送需要拿到chat_id或open_id,这些在事件体里都有。但要注意,不同事件类型里字段位置不一样,群聊消息和单聊消息的结构有差异,解析时要做兼容。我封装了一个统一的"提取会话标识"函数,把差异挡在外面。

4. 工具设计:决定智能体上限的不是模型,是你给它的工具

4.1 工具描述写得好不好,直接决定调用准确率

很多人以为智能体聪明不聪明全看模型,其实工具描述的影响大到超乎想象。我做过对比实验:同一个模型,同一批问题,只改工具描述,调用准确率能从六成提到九成以上。

好的工具描述要回答三个问题:这个工具是干什么的、什么时候该用它、参数怎么填。举个例子,我有个查文档的工具,最初的描述是"查询文档"。模型经常在该用搜索的时候用了它,因为"查询"和"搜索"在语义上太接近。后来我改成"根据文档的唯一标识精确获取文档全文,当你已经知道文档 ID、需要读取具体内容时使用;如果你只有关键词、不知道文档 ID,请改用搜索工具"。改完之后,误用率大幅下降。

关键技巧是在描述里主动做"排除",告诉模型什么情况下不要用这个工具,以及应该改用哪个。模型没有你想象的那么会"举一反三",把边界划清楚,它才不容易越界。

4.2 参数设计:宁可多一个必填项,不要一堆可选参数

工具参数的设计也有讲究。我一开始喜欢把参数都设成可选,觉得灵活。结果模型经常漏填关键参数,或者填了互相矛盾的组合。后来我改成:能必填的就必填,可选参数控制在两个以内。

必填参数逼着模型在调用前把信息凑齐,凑不齐它就会先追问用户,这恰恰是我们想要的行为。可选参数太多则会让模型陷入"选择困难",输出一堆没用的默认值。

另外,参数类型要明确。字符串、数字、布尔、枚举,该是什么就是什么。枚举类型特别有用,比如"排序方式"就限定为["按时间", "按相关度"],模型就不会自由发挥写出"按重要性"这种你没法处理的值。

4.3 工具执行失败怎么办:把错误信息也当成上下文

工具调用不可能永远成功,数据库连不上、接口超时、参数非法,都会失败。新手常见的做法是失败了就返回一句"操作失败",然后循环继续,模型一脸茫然。

正确的做法是把失败原因详细返回给模型。比如"查询失败:文档 ID 不存在,请确认 ID 是否正确,或改用搜索工具按关键词查找"。模型拿到这个信息后,往往能自己调整策略,换个工具或换个参数重试。这相当于给智能体加了"自我纠错"能力,而成本只是把错误信息写清楚。

我在工具执行层统一包了一层异常处理,任何异常都转成结构化的错误描述返回,绝不让异常直接抛出打断整个循环。这个设计让智能体在面对不稳定依赖时表现得相当稳健。

5. 上下文管理:智能体"记性"好坏的分水岭

5.1 上下文不是越长越好,而是越"干净"越好

刚开始我图省事,把所有历史消息、所有工具调用结果全塞进上下文。跑了几轮之后发现两个问题:一是 token 消耗飞快,成本扛不住;二是模型开始"迷失",被一堆无关的历史干扰,回答质量反而下降。

后来我改成滑动窗口 + 关键信息摘要的策略。保留最近 N 轮完整对话,更早的内容压缩成一段摘要。摘要只保留"用户的核心诉求"和"已经确认的关键结论",工具调用的中间过程全部丢弃。这样上下文既不会无限膨胀,又不会丢失关键信息。

具体 N 取多少,我的经验是 6 到 10 轮比较合适。太少了模型容易忘记前文,太多了又会引入噪声。这个值可以根据实际对话长度动态调整,简单任务用小窗口,复杂任务用大窗口。

5.2 工具调用结果要不要全量保留

工具返回的结果往往很长,比如一次搜索返回十条结果,每条几百字。全塞进上下文,几轮下来就爆了。我的处理是在工具层做一次"预压缩":搜索结果只保留标题和摘要,完整内容等模型真的需要时再单独取。

这个思路叫"懒加载"。模型先看到"有这么几条结果",决定要看哪条,再调用读取工具拿全文。这样既省 token,又逼着模型做更精准的决策。实测下来,这个改动让单次对话的平均 token 消耗降了一半以上,而回答质量几乎没受影响。

5.3 循环的终止条件:怎么防止智能体"死循环"

智能体最危险的情况是陷入死循环:调工具、失败、再调、再失败,无限重复。我见过最夸张的一次,模型在"搜索—没找到—换个词再搜"之间来回折腾了二十多轮。

防死循环要设两道闸。第一道是硬性轮次上限,比如最多循环 8 轮,到了就强制结束并返回当前最好的结果。第二道是重复检测,如果连续两轮的动作和参数完全一样,直接判定为卡住,中断循环并提示模型换个思路。

这两道闸之外,我还会在系统提示里明确告诉模型"如果连续两次尝试都没有进展,请直接告诉用户你遇到了困难,而不是继续尝试"。模型其实挺听话的,把规则说清楚,它大部分时候会遵守。

6. 实测中的意外与调优:那些只有跑起来才知道的事

6.1 模型"自作主张"编造工具结果

有一次测试,我故意把某个工具禁用,想看模型怎么办。结果它没报错,而是自己编了一段看起来很像真的结果返回给用户。这个行为很危险,因为用户根本分不清真假。

根因是模型在训练时被鼓励"给出答案",当工具不可用时,它倾向于"脑补"一个答案而不是承认失败。解决办法是在系统提示里强硬地规定:所有事实性信息必须来自工具返回,工具没返回的内容一律回答"我无法确认"。同时,在代码层做校验,如果模型声称用了某个工具但实际没调用,直接拦截。

这个坑让我意识到,智能体的"诚实"不能靠模型自觉,得靠工程手段兜底。

6.2 中文标点和 JSON 解析的恩怨

强制模型输出 JSON 时,我遇到过各种奇葩情况:中文引号、末尾多余逗号、字段名大小写不一致、甚至把 JSON 包在 markdown 代码块里。直接json.loads十次有三次报错。

我的处理是解析前先做一轮清洗:去掉代码块标记、把中文标点替换成英文、用宽松的解析库容错。清洗之后再做严格解析,失败的话把错误信息返回给模型让它重出一次。这个"重试一次"的机制很关键,大部分格式错误模型自己就能修好。

6.3 响应速度:用户能忍多久

智能体因为要循环多轮,响应天然比普通机器人慢。实测下来,一轮工具调用加模型推理大概 2 到 4 秒,三轮就是十几秒。用户等十几秒是会烦躁的。

我的优化手段有三个。一是流式输出,模型一边生成一边推给用户,让用户看到"它在动"。二是先回执后结果,前面提过的"收到,正在处理"。三是并行调用,如果一轮里要调多个互不依赖的工具,并发执行而不是串行。这三招下来,体感速度快了很多,虽然总耗时没变,但用户不再觉得"卡死"了。

6.4 成本控制:别让智能体变成烧钱机器

循环调用模型,token 消耗是普通问答的好几倍。我做过统计,一个中等复杂度的任务,平均要消耗几千到上万 token。如果不加控制,月底账单会很难看。

控制成本的手段:用小模型做决策,用大模型做最终回答。决策只需要输出一个 JSON 动作,小模型完全够用;最终回答才需要大模型的语言能力。这个拆分能省下大半成本。另外,缓存高频问题的结果,很多问题是重复的,命中缓存直接返回,连模型都不用调。

7. 从能跑到好用:我总结的几条实战心得

7.1 先跑通最小闭环,再谈优化

我见过不少人一上来就追求完美:要支持多轮、要支持多工具、要支持记忆、要支持各种边界情况。结果代码写了一堆,一个都没跑通,最后不了了之。

正确的节奏是先做一个只能调用一个工具、只能循环一轮的最小版本,确保消息能进、模型能调、结果能出。这个闭环跑通了,再往上加工具、加轮次、加记忆。每加一个能力,都确保前面的还能正常工作。这样出问题时,你永远知道是哪个改动引入的。

7.2 日志要记全,尤其是模型的原始输出

调试智能体最痛苦的是"它为什么这么决策"。如果只记最终结果,你根本无从下手。我的做法是把每一轮模型的原始输出、解析后的动作、工具的执行结果、耗时全部记下来。出问题时翻日志,一眼就能看出是哪一步跑偏了。

日志级别我分了三档:正常流程记 INFO,模型输出异常记 WARN,工具执行失败记 ERROR。这样平时看 INFO 了解流程,出问题直接过滤 WARN 和 ERROR。

7.3 给智能体一个"人格"和"边界"

纯功能性的智能体用起来很生硬。我在系统提示里给它加了一段"人设":说话简洁、不啰嗦、遇到不确定的事直说、不拍马屁。同时明确边界:不回答与工作无关的问题、不编造信息、不代替人做决策。

这段提示看起来是"软"的,但效果很"硬"。加了之后,回答风格统一了,用户反馈也好了很多。关键是边界那段,直接减少了一大类"越界回答"的问题。

7.4 别急着上多智能体

现在很流行"多智能体协作",一个负责规划、一个负责执行、一个负责审核。我试过,对于团队内部这种中等复杂度的场景,单智能体加好工具完全够用,多智能体反而引入了通信开销和协调复杂度,得不偿失。

多智能体适合那种任务边界非常清晰、需要不同"专家"分工的场景。如果你的需求只是"查资料、答问题、调接口",老老实实把单智能体的工具做好,比堆智能体数量有用得多。

7.5 上线只是开始,持续观察才是重头戏

智能体上线后,我每天都会看几类数据:哪些问题被问得最多、哪些工具调用失败率最高、哪些对话轮次异常多。这些数据直接指向优化方向。比如发现某个工具失败率高,就去查是描述不清还是实现有 bug;发现某类问题轮次特别多,说明工具设计得不够直接,模型绕了远路。

这套"观察—定位—优化"的循环,比一开始就设计得完美更重要。智能体是个需要养的东西,养的过程就是不断根据真实数据微调的过程。

8. 写在最后的一点个人体会

这套东西我从动手到稳定运行,前后折腾了大概三周,其中一半时间花在飞书适配和踩坑上,真正核心逻辑的打磨反而没占多少。这也印证了我一开始的判断:智能体的难点不在"智能",而在"工程"。模型能力是现成的,怎么把它稳稳当当地接进真实业务流里,才是真正考验人的地方。

那 100 行核心代码我改了很多版,每一版都是被真实问题逼出来的。如果你也想动手做一个,我的建议是别追求一步到位,先让它在你的飞书群里跑起来,哪怕只会回答一个问题。跑起来之后,你会遇到一堆文档里不会写、只有真实使用才会暴露的问题,而解决这些问题的过程,才是你真正掌握智能体的过程。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询