news 2026/10/20 6:48:51

100行代码手搓飞书智能体:极简工具调用循环实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
100行代码手搓飞书智能体:极简工具调用循环实战

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

1.1 从“玩具”到“工具”的落差

过去一年我陆陆续续试过不少智能体框架,从早期的 AutoGPT 到后来的各类 Agent 编排平台,说实话,演示视频都很惊艳,但真正落到日常工作中,能持续用下去的没几个。大部分框架要么依赖一大堆外部服务,要么配置复杂到让人失去耐心,要么就是“看起来很智能,实际用起来很笨”——你让它查个天气,它给你绕三圈调用五个工具,最后还答非所问。

我真正想要的东西其实很朴素:一个能挂在我日常用的聊天工具里、能理解自然语言、能调用我预设的几个工具、能记住上下文、代码量可控、出了问题我能自己改的智能体。不需要多模态,不需要花哨的界面,不需要复杂的编排引擎。就是那种“我发一句话,它帮我干活”的东西。

Hermes 式智能体的核心思路给了我很大启发——它强调用极简的循环结构驱动大模型,通过工具调用完成实际任务,而不是堆砌复杂的架构。我决定沿着这个思路,用最少的代码实现一个能接入飞书的版本。最终核心逻辑压缩到 100 行左右,剩下的都是配置和适配代码。

1.2 这个智能体到底能做什么

先把这个东西的能力边界说清楚,免得你抱有不切实际的期待。它本质上是一个“消息驱动的工具调用循环”,具体来说:

  • 你在飞书里给它发一条消息,比如“帮我查一下今天北京的天气”
  • 它把这条消息连同系统提示词一起发给大模型
  • 大模型判断需要调用天气查询工具,返回一个工具调用请求
  • 智能体执行这个工具,把结果再喂回给大模型
  • 大模型根据工具返回的结果生成最终回复
  • 回复通过飞书机器人发回给你

整个过程就是一个 while 循环,直到大模型不再请求调用工具为止。听起来简单,但这个循环能覆盖的场景非常多:查资料、做计算、读写文件、调用内部接口、定时提醒等等。关键在于你给它配了哪些工具。

适合谁来参考这篇内容?如果你是有一定编程基础、想快速搭一个自己能掌控的智能体、并且日常使用飞书作为沟通工具的开发者,那这篇东西就是写给你的。如果你完全不懂编程,可能需要先补一下 Python 基础和 HTTP 接口的基本概念。

1.3 为什么选飞书作为接入渠道

这里说一下选型逻辑。市面上能接入的聊天平台不少,我选飞书主要基于几个考虑:

第一,飞书的机器人开发文档相对清晰,自建应用的流程不复杂,个人开发者也能申请。第二,飞书的开放平台接口设计比较规范,消息收发、事件订阅这些都有标准的 HTTP 回调机制,不需要额外的长连接库。第三,飞书的消息卡片能力很强,后续如果想做交互式的按钮、表单,扩展空间大。第四,我日常工作流本身就在飞书上,接入之后不需要切换工具,使用成本最低。

当然,如果你用的是其他聊天工具,核心逻辑完全一样,只需要替换消息收发的适配层。智能体本身跟平台是解耦的,这一点在设计时就要注意。

2. 整体架构与核心设计思路

2.1 一百行核心代码的边界在哪里

先把“100 行核心代码”这个概念说清楚,免得产生误解。这 100 行指的是智能体的主循环逻辑:接收消息、调用大模型、处理工具调用、返回结果。不包括:

  • 飞书的消息收发适配代码(大约 80 行)
  • 工具函数的具体实现(每个工具 10 到 30 行不等)
  • 配置文件和依赖管理
  • 错误处理和日志

所以整个项目加起来大概 300 到 400 行,但真正体现“智能体”这个概念的,就是那 100 行左右的主循环。这样拆分的好处是,核心逻辑足够清晰,你读一遍就能理解智能体是怎么运转的,不会被各种适配代码干扰。

我见过太多项目把核心逻辑和平台适配、工具实现混在一起,导致想改一个地方要翻半天代码。分层设计不是过度工程,而是让自己以后能看懂自己写的东西。

2.2 消息驱动的循环结构

整个智能体的运转逻辑可以用一句话概括:收到消息就进入循环,循环里不断问大模型“你还需要调工具吗”,直到它说“不需要了”为止。

用伪代码表示大概是这样:

def handle_message(user_message, history): messages = build_messages(user_message, history) while True: response = call_llm(messages) if response.has_tool_calls: for tool_call in response.tool_calls: result = execute_tool(tool_call) messages.append(tool_result_message(result)) else: return response.content

这个循环的精髓在于:大模型是决策者,代码只是执行者。你不需要写一堆 if-else 来判断用户想干什么,大模型会根据系统提示词和工具描述自己决定。这就是 Hermes 式智能体的核心思想——把决策权交给模型,把执行权留给自己。

为什么用 while 循环而不是递归?因为递归在工具调用链较长时容易栈溢出,而且调试起来不直观。while 循环配合一个最大迭代次数限制,既安全又好排查。

2.3 工具注册机制的设计

工具是智能体的手脚。没有工具,大模型只能聊天;有了工具,它才能干活。我的设计是每个工具用一个 Python 函数实现,通过装饰器注册到工具表里,同时自动生成工具的 JSON Schema 描述供大模型使用。

TOOLS = {} def tool(name, description, parameters): def decorator(func): TOOLS[name] = { "function": func, "schema": { "type": "function", "function": { "name": name, "description": description, "parameters": parameters } } } return func return decorator

这样设计的好处是,新增一个工具只需要写一个函数加一个装饰器,不需要改任何核心逻辑。工具的描述直接写在装饰器里,大模型通过这个描述来判断什么时候该调用哪个工具。描述写得好不好,直接决定了工具调用的准确率,这一点后面会详细说。

2.4 上下文管理的取舍

上下文管理是个容易被忽视但很关键的问题。大模型的上下文窗口有限,如果每次对话都把全部历史塞进去,很快就会超限。我的做法是:

  • 保留最近 N 轮对话(默认 10 轮)
  • 系统提示词始终保留
  • 工具调用的中间结果在当轮结束后丢弃,只保留最终回复

为什么不保留工具调用记录?因为那些中间结果对后续对话通常没有价值,反而会占用大量 token。比如查天气的工具返回了一长串 JSON,用户下一句问“那明天呢”,大模型只需要知道“刚才查过天气”就够了,不需要看到完整的 JSON 数据。

当然这是个取舍。如果你的场景需要大模型记住工具返回的具体数据,那就得保留。我建议先用丢弃策略跑一段时间,发现确实需要再改,不要一上来就做复杂的上下文压缩。

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

3.1 创建飞书自建应用的步骤

第一步是去飞书开放平台创建一个自建应用。这个过程不复杂,但有几个坑需要注意。

进入开发者后台,选择“创建企业自建应用”,填写应用名称和描述。名称随便起,但建议跟你实际用途相关,比如“我的助手”。创建完成后,你会拿到 App ID 和 App Secret,这两个东西后面要用。

接下来是配置权限。在“权限管理”页面,你需要开通以下权限:

  • im:message:获取与发送单聊、群组消息
  • im:message.group_at_msg:接收群聊中@机器人消息
  • im:message.p2p_msg:接收单聊消息

如果你还想让机器人能主动发消息,还需要im:message:send_as_bot权限。权限开通后需要发布版本才能生效,这一步别忘了。

然后是事件订阅。在“事件订阅”页面,你需要配置请求地址,也就是你的服务端接收飞书回调的 URL。这里有个关键点:飞书会先发一个验证请求过来,你的服务端需要正确响应 challenge 才能通过验证。很多人卡在这一步,后面会详细说怎么处理。

3.2 消息收发的适配层实现

飞书的消息回调是 HTTP POST 请求,body 是 JSON 格式。你需要做三件事:验证请求、解析消息、回复消息。

验证请求的处理逻辑是:飞书发过来的 JSON 里有一个challenge字段,你只需要原样返回这个值即可。注意要设置正确的 Content-Type 为application/json。

@app.route('/webhook', methods=['POST']) def webhook(): data = request.json if 'challenge' in data: return jsonify({'challenge': data['challenge']}) # 处理消息 handle_event(data) return jsonify({'code': 0})

解析消息时要注意,飞书的事件格式有两层嵌套:外层是事件类型,内层是具体内容。消息事件的关键字段在event.message.content里,是一个 JSON 字符串,需要二次解析。

回复消息需要先获取 tenant_access_token,然后用这个 token 调用发送消息接口。token 有有效期,建议缓存起来,不要每次发消息都重新获取。

def get_tenant_token(): if cache.get('token'): return cache['token'] resp = requests.post( 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', json={'app_id': APP_ID, 'app_secret': APP_SECRET} ) token = resp.json()['tenant_access_token'] cache['token'] = token cache['expire'] = time.time() + 7000 return token

注意:tenant_access_token 的有效期是 7200 秒,建议提前 200 秒刷新,避免边界情况导致请求失败。

3.3 大模型接口的对接细节

我用的是 OpenAI 兼容的接口格式,这样切换模型供应商时只需要改 base_url 和 model 名称。核心是构造正确的请求体:

def call_llm(messages): resp = requests.post( f'{BASE_URL}/chat/completions', headers={'Authorization': f'Bearer {API_KEY}'}, json={ 'model': MODEL_NAME, 'messages': messages, 'tools': [t['schema'] for t in TOOLS.values()], 'tool_choice': 'auto' } ) return resp.json()['choices'][0]['message']

这里有几个参数值得说明。tools字段传入所有注册工具的 schema,大模型会根据这些描述决定是否调用。tool_choice设为auto表示让模型自己决定,如果你想强制它必须调用某个工具,可以设为具体值,但一般不建议。

响应解析时要注意,如果模型决定调用工具,返回的 message 里会有tool_calls字段,是一个数组,可能包含多个工具调用。你需要逐个执行,然后把结果按顺序追加到 messages 里。

3.4 工具调用的完整链路演示

拿“查天气”这个场景走一遍完整链路,你就明白整个流程了。

用户发消息:“北京今天天气怎么样”。适配层收到后,构造 messages 数组,包含系统提示词和用户消息。调用大模型,模型返回一个 tool_call,name 是get_weather,arguments 是{"city": "北京"}。

智能体解析这个 tool_call,找到对应的函数执行,得到结果{"temp": 25, "condition": "晴"}。然后构造一条 role 为tool的消息,带上 tool_call_id 和结果内容,追加到 messages 里。再次调用大模型,这次模型不再请求工具,直接返回“北京今天晴天,气温 25 度”。

整个过程中,用户只发了一条消息,但背后发生了两次大模型调用和一次工具执行。这就是智能体的工作方式——对用户透明,但内部有完整的决策链路。

4. 工具设计与提示词工程

4.1 工具描述怎么写才准确

工具描述的质量直接决定大模型调用的准确率。我踩过的坑是:描述写得太简单,模型不知道什么时候该用;描述写得太复杂,模型又容易混淆。

好的工具描述应该包含三个要素:这个工具做什么、什么时候用、参数是什么意思。比如查天气的工具:

@tool( name="get_weather", description="查询指定城市当天的天气情况,包括温度和天气状况。当用户询问天气相关问题时使用。", parameters={ "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,如北京、上海" } }, "required": ["city"] } ) def get_weather(city): # 实现略 pass

注意 description 里明确写了“当用户询问天气相关问题时使用”,这就是在告诉模型调用时机。参数描述里给了示例,帮助模型正确填充参数。

我实测下来,描述里加上调用时机的说明,工具调用的准确率能提升不少。不加的话,模型有时候会该调不调,或者不该调乱调。

4.2 系统提示词的关键作用

系统提示词是智能体的“人格设定”,它决定了智能体的行为风格和边界。我的系统提示词大概是这样:

你是一个乐于助人的智能助手,可以通过调用工具来帮助用户完成任务。 当用户的问题需要实时信息或计算时,优先调用相应工具。 如果工具调用失败,如实告知用户,不要编造结果。 回答要简洁直接,不要过度解释。

这几句话看似简单,但每一句都有用意。“优先调用工具”是防止模型偷懒直接编答案。“不要编造结果”是防止工具失败时模型胡诌。“简洁直接”是控制回复长度,避免啰嗦。

你可以根据自己的需求调整这个提示词。比如你想让它更活泼,可以加“用轻松的语气回复”;你想让它更严谨,可以加“不确定的信息要明确说明”。

4.3 多工具场景下的选择逻辑

当你有多个工具时,模型需要判断该调用哪个。这时候工具描述的区分度就很重要。我建议:

  • 工具名称要有辨识度,不要用tool1、tool2这种
  • 功能相近的工具,在描述里明确区分使用场景
  • 参数设计尽量统一,减少模型的认知负担

举个例子,如果你同时有“查天气”和“查空气质量”两个工具,描述里要写清楚:天气工具用于温度、降水等问题,空气质量工具用于 PM2.5、AQI 等问题。不要指望模型自己猜。

如果工具数量超过 10 个,建议做分组或者用更高级的路由机制。不过对于个人使用场景,5 到 8 个工具基本够用了,太多反而增加模型判断的难度。

4.4 工具执行的安全边界

工具是智能体跟外部世界交互的通道,必须设置安全边界。我的做法是:

  • 文件操作限制在指定目录内,不允许访问系统目录
  • 网络请求设置超时,避免卡死
  • 敏感操作(如删除文件)需要二次确认
  • 工具执行结果做长度截断,避免超长内容撑爆上下文

这些限制不是不信任大模型,而是防止意外。大模型有时候会“过度热情”,你问它一个问题,它可能调用一堆工具去查。设置边界能避免这种情况造成实际损害。

提示:工具函数里一定要做异常捕获,返回结构化的错误信息而不是直接抛异常。否则一个工具报错会导致整个循环崩溃。

5. 常见问题与排查实录

5.1 飞书回调验证失败怎么办

这是最常见的第一个坑。飞书配置事件订阅时,会发一个验证请求,你的服务端必须正确返回 challenge 值。常见失败原因:

问题现象可能原因解决方法
提示 URL 验证失败服务端没有返回 challenge检查代码是否正确解析并返回
提示请求超时服务端响应太慢确保验证逻辑在 3 秒内返回
提示格式错误Content-Type 不对设置为 application/json
验证通过但收不到消息权限未发布去权限管理页面发布版本

我遇到过一次验证失败,排查半天发现是服务端框架自动加了 BOM 头,导致飞书解析失败。这种问题很隐蔽,建议用 curl 手动测试一下接口返回的原始内容。

5.2 大模型不调用工具怎么破

有时候你明明配了工具,但模型就是不用,直接凭自己的知识回答。这种情况通常是三个原因:

第一,工具描述不够明确。模型不知道什么时候该用这个工具。解决办法是在 description 里明确写调用时机。

第二,系统提示词没有强调。在系统提示词里加一句“需要实时信息时优先调用工具”,能明显改善。

第三,模型本身的能力问题。有些小模型对工具调用的支持不好,换个模型试试。我用下来,支持 function calling 的模型在这方面表现都不错。

如果还是不行,可以临时把tool_choice设为具体工具名,强制模型调用。但这只是调试手段,不要长期这么用。

5.3 工具调用结果太长导致上下文溢出

这个问题在调用返回大量数据的工具时特别明显。比如你调用一个搜索工具,返回了 5000 字的网页内容,直接塞进 messages 里,下一轮调用就可能超限。

我的处理方式是:在工具函数里做结果截断,只返回关键信息。比如搜索工具只返回前三条结果的标题和摘要,而不是全文。如果确实需要完整内容,让模型分多次调用,每次取一部分。

另一个技巧是在系统提示词里加一句“工具返回结果可能被截断,如需完整信息请分步获取”。这样模型知道结果不完整时,会主动分步调用。

5.4 消息重复回复的问题

飞书的事件订阅有个机制:如果你没有在指定时间内返回 200,飞书会重试推送。如果你的处理逻辑比较慢,就可能出现同一条消息被处理两次、回复两次的情况。

解决办法是加一个消息去重机制。飞书的事件里有一个event_id字段,你可以用一个集合记录最近处理过的 event_id,重复的直接跳过。

processed_events = set() def handle_event(data): event_id = data.get('header', {}).get('event_id') if event_id in processed_events: return processed_events.add(event_id) # 处理消息

注意这个集合要定期清理,不然会一直增长。简单做法是只保留最近 1000 个,或者用带过期时间的缓存。

5.5 排查问题的通用思路

遇到问题时的排查顺序建议是:

  1. 先看日志,确认请求有没有到达服务端
  2. 确认请求内容格式是否正确
  3. 检查大模型调用是否成功,返回内容是什么
  4. 检查工具执行是否报错
  5. 检查回复消息是否成功发送

我习惯在每个关键节点打日志,包括收到的原始请求、构造的 messages、大模型返回、工具执行结果、发送的回复。这样出问题时能快速定位是哪一环出了问题。

提示:日志里不要打印完整的 API Key 和用户隐私信息,调试完成后记得清理敏感日志。

6. 扩展方向与个人经验

6.1 可以继续加的东西

这个智能体的基础框架搭好之后,扩展空间很大。我目前想到的几个方向:

定时任务:加一个定时器,让智能体在指定时间主动发消息。比如每天早上推送天气和日程。实现方式是用一个后台线程检查任务队列,到时间就调用发送消息接口。

多轮对话记忆:目前只保留最近 10 轮,如果想让它记住更久之前的信息,可以加一个向量数据库做长期记忆。用户提到的重要信息存进去,需要时检索出来。

消息卡片交互:飞书的消息卡片支持按钮和表单,可以让智能体发带按钮的消息,用户点击后触发特定操作。这在做审批、选择这类场景时很有用。

多用户隔离:目前是单用户设计,如果要给多个人用,需要按用户 ID 隔离上下文和工具权限。这个改动不大,主要是加一个用户维度的存储。

6.2 我踩过的几个坑

说几个实际踩过的坑,希望能帮你省点时间。

第一个坑是 token 缓存没做,每次发消息都重新获取 tenant_access_token,结果飞书那边有频率限制,发多了就被限流。后来加了缓存,问题解决。

第二个坑是工具函数没有做超时控制,有一次调用一个外部接口卡住了,整个智能体就挂在那里不动。后来给所有网络请求加了 timeout 参数。

第三个坑是系统提示词写得太长,占了大量 token,导致实际对话空间被压缩。后来精简到几句话,效果反而更好。提示词不是越长越好,关键是每句话都有用。

第四个坑是没做消息去重,飞书重试导致重复回复。这个前面说过了,加个 event_id 去重就行。

6.3 关于代码量的一点体会

最后说回“100 行核心代码”这个事。我一开始也觉得 100 行能写出什么,但实际做下来发现,核心逻辑确实可以很精简。关键在于把复杂度放在该放的地方:工具实现可以复杂,适配层可以复杂,但主循环必须简单。

主循环简单的好处是,你随时能看懂它在干什么,出问题能快速定位,想改行为只需要调提示词或加工具,不用动核心逻辑。这种“核心极简、外围灵活”的架构,我觉得是个人开发者做智能体最务实的路线。

不要追求一开始就做一个大而全的框架,先跑通最小闭环,然后根据实际需求一点点加。我这个小智能体现在每天帮我处理查资料、做计算、定时提醒这些杂事,虽然不复杂,但确实省了不少切换工具的时间。这就够了。

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

Delphi连接Oracle终极方案:ODAC直连、REF CURSOR映射与时区控制

简介:本资源为Delphi与CBuilder开发者专用的ODAC(Oracle Data Access Components)完整组件包,聚焦Oracle数据库高效接入与开发,适用于中高级桌面应用开发者快速构建企业级数据库系统。包内含1807个文件,主体…

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

深入 Flutter Tools:揭秘 flutter 命令背后的构建中枢与源码调试

1. 这不是“命令行工具”,而是 Flutter 的中枢神经系统你敲下flutter build apk,它就真能打包出一个安装包;你输入flutter run,手机上立刻跑起热重载界面;你执行flutter pub get,依赖树瞬间拉取完毕——这些…

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

Hermes智能体开发入门:先掌握这4个核心基础

1. 为什么“先学这4件事”不是标题党1.1 三个月踩坑之后的核心结论Hermes 这个项目,我前后用了三个月。前一个月基本在瞎折腾,装完跑通一个 demo 就以为入门了,结果一碰到真实场景就原形毕露。后来我复盘了一下,发现真正卡住新手的…

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

Git本地仓库与远程仓库的本质区别与协同原理

1. 为什么“每天1分钟”不是营销话术,而是Git入门最真实的节奏你有没有试过打开Git官方文档,看到“Working Directory → Staging Area → Repository → Remote”这串箭头,脑子就自动卡在第二步?或者刚学会git add .和git commit…

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

北京旅游避坑指南:家庭出游如何选对讲解服务,告别低价团和排队坑

先说个很现实的场景:家里长辈一句“想去北京看看”,听起来就是一张机票的事,可实际从定日子、约门票、排路线到选讲解,每一步都能踩出不同花样的坑。我身边这两个月至少有三位朋友,都因为“带爸妈去北京”这件事提前焦…

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

AI日报盘点:多模态统一架构、端侧7B推理与多智能体协作新进展

今天的 AI 圈相当热闹,正好赶上多模态大模型扎堆更新、端侧推理跑出新纪录、AI 智能体在办公场景里进一步落地。我花了一整天蹲在各个技术社区的更新流、跑了几组实测,整理了 2026 年 1 月 28 日的这份 AI 日报,重点聊聊那些真正值得关注的变…

作者头像 李华