☰
飞书机器人接入本地RAGFlow知识库:从部署到长连接保活的完整实践
2026/10/2 4:33:59 网站建设 项目流程

1. 为什么我要把飞书机器人和本地 RAGFlow 知识库接起来

公司内部的知识散落在飞书文档、Confluence、各种 PDF 和 Markdown 里,每次有人问“XX 流程怎么走”“XX 接口的参数是什么”,都要在群里 @ 相关同事,问的人累,答的人也累。我一开始想的是直接买个 SaaS 知识库,但数据合规过不了,内部文档不可能往外传。后来试了 Dify、WeKnora 这些开源方案,最后选了 RAGFlow,原因是它对复杂版式 PDF 的解析确实比同类强一截,尤其是带表格和扫描件的那种文档,解析出来的 chunk 质量明显更高。

但 RAGFlow 有个问题:它本身是个 Web 界面,同事不会为了问一个问题专门去打开一个网页、登录、再输入。大家日常就在飞书里,消息发出去就希望有回复。所以我的目标很明确——在飞书里 @ 一下机器人,它去本地 RAGFlow 知识库检索,把答案带引用返回。整条链路跑通之后,同事的使用率从“没人用”变成了“每天都在问”,这个变化比技术本身更有意思。

这篇文章我会把整条链路拆开讲:飞书机器人怎么建、RAGFlow 怎么部署和调优、中间的 AI 智能体服务怎么写、WebSocket 长连接怎么保活、踩了哪些坑。适合有一定 Python 基础、想自己搭一套内部问答系统的朋友。如果你完全没碰过飞书开放平台或者 RAGFlow,也能跟着走,我会把每一步的“为什么”讲清楚。

2. 飞书机器人从创建到能收消息的完整路径

2.1 企业自建应用 vs 商店应用:选错了后面全是坑

飞书开放平台创建应用时有两个选项:企业自建应用和商店应用。很多人第一次会犹豫,我直接说结论——内部知识库问答场景,必须选企业自建应用。商店应用是给第三方开发者发布到飞书应用市场用的,审核流程长,权限申请也更严格,而且它的消息接收机制和企业自建应用不一样。

创建完之后,你需要在“凭证与基础信息”页面拿到两个关键东西:App ID和App Secret。这两个后面写代码要用。App Secret 千万不要提交到 Git,我一般放在环境变量或者.env文件里,.gitignore里加上.env。

2.2 权限配置:少一个权限,机器人就是哑巴

飞书机器人的权限分两块:发消息的权限和收消息的权限。很多人只配了发消息,结果机器人能主动推送但收不到用户消息,就是因为没开接收权限。

我实际用到的权限清单如下:

权限名称权限标识用途
获取与发送单聊、群组消息im:message接收用户消息、发送回复
以应用的身份发消息im:message:send_as_bot机器人身份发送消息
读取用户发给机器人的单聊消息im:message.p2p_msg:readonly接收私聊消息
获取群组中所有消息im:message.group_msg接收群聊中 @ 机器人的消息
上传图片或文件im:resource发送图片、文件类型的回复

配完权限后一定要点“创建版本并发布”,不发布的话权限不生效。我第一次就是配了权限没发布,调试了半小时以为代码有问题,结果发现是权限没生效。

2.3 事件订阅:长连接模式为什么比 Webhook 更适合本地部署

飞书接收消息有两种模式:Webhook 模式和长连接模式。

Webhook 模式需要你有一个公网可访问的 HTTPS 地址,飞书把消息 POST 过来。但我的 RAGFlow 和智能体服务都跑在本地内网,没有公网 IP,也不想为了这个专门搞内网穿透。所以选了长连接模式——由你的服务主动向飞书建立 WebSocket 连接,消息通过这条连接推过来。

长连接模式的好处很明显:不需要公网地址、不需要处理签名验证、不需要担心回调超时。但代价是你得自己维护连接的心跳和重连。飞书官方提供了 Python SDKlark-oapi,里面封装了长连接的客户端,省了不少事。

在飞书后台“事件订阅”页面,选择“使用长连接接收事件”,然后添加事件im.message.receive_v1。这个事件就是用户给机器人发消息时触发的。

2.4 机器人菜单和快捷指令:让同事知道怎么用

光有机器人还不够,得让同事知道怎么触发。我在机器人配置里加了自定义菜单,配了几个常用指令,比如“查流程”“查接口”“查规范”。用户点菜单就能直接发对应的问题,不用自己组织语言。这个细节看起来小,但实际使用率提升很明显——很多人不是不会问,是不知道该问什么。

3. RAGFlow 本地部署:Docker 编排里的隐藏细节

3.1 硬件门槛和镜像选择

RAGFlow 对硬件有要求,官方建议至少 16GB 内存、4 核 CPU。我实测下来,如果只是解析文档和做检索,16GB 勉强够用,但同时跑嵌入模型和重排序模型的话,内存会吃紧。我的机器是 32GB 内存,跑起来比较从容。

Docker 部署直接用官方仓库的docker-compose.yml。但这里有个坑:默认的嵌入模型是 BAAI/bge-large-zh-v1.5,下载需要从 HuggingFace 拉。如果你的网络环境访问 HuggingFace 不稳定,建议提前把模型下载好放到本地,然后修改docker-compose.yml里的模型路径。

# 拉取 RAGFlow 源码 git clone https://github.com/infiniflow/ragflow.git cd ragflow/docker # 修改 .env 文件,指定模型路径 # 把 HF_ENDPOINT 改成国内镜像,或者提前下载模型

3.2 解析配置:chunk 大小和重叠度怎么定

RAGFlow 的解析质量很大程度上取决于chunk 大小和重叠度这两个参数。默认 chunk 是 512 token,重叠 128 token。但我实际用下来,内部文档场景下这个默认值不一定最优。

我的经验是:

  • 技术文档、API 文档:chunk 设小一点,256-384 token,因为技术文档信息密度高,一个 chunk 太大容易混入无关内容。
  • 流程规范、制度文档:chunk 可以设大一点,512-768 token,因为这类文档上下文依赖强,切太碎反而丢信息。
  • 带表格的文档:RAGFlow 对表格有专门的处理,但前提是解析时选了“表格”选项。如果表格没被正确识别,检索时表格内容基本等于丢失。

在 RAGFlow 的“知识库配置”里,解析方法选“General”还是“Manual”也有讲究。General 是自动分块,Manual 是手动指定分隔符。我一般先用 General 跑一遍,看看分块效果,如果发现某些文档切得不好,再单独对那个文档用 Manual 调整。

3.3 检索策略:向量检索 + 关键词检索的混合模式

RAGFlow 支持多种检索方式:向量检索、关键词检索、混合检索。我一开始只用向量检索,后来发现有些专有名词、产品代号,向量检索召回率很低,因为嵌入模型没见过这些词。加上关键词检索之后,这类查询的命中率明显提升。

RAGFlow 的混合检索是通过 RRF(Reciprocal Rank Fusion)把两路结果融合的。实际配置里,我一般把向量检索权重设 0.7,关键词检索权重设 0.3。这个比例不是固定的,如果你的文档里专有名词特别多,可以把关键词权重调到 0.4-0.5。

还有一个参数是Top N,就是返回多少个 chunk 给大模型。设太小,可能漏掉关键信息;设太大,大模型上下文塞太多无关内容,反而影响回答质量。我一般设 5-8 个,然后让重排序模型再过一遍。

4. AI 智能体服务:从收到消息到返回答案的完整逻辑

4.1 整体架构:为什么中间要加一层智能体服务

有人可能会问:飞书机器人直接调 RAGFlow 的 API 不就行了吗,为什么要中间加一层?

原因是 RAGFlow 的 API 返回的是检索结果,不是自然语言答案。你需要把检索到的 chunk 拼成 prompt,调大模型生成回答,再把回答发回飞书。这一层逻辑就是智能体服务干的活。另外,这一层还负责:

  • 意图识别:判断用户是在问知识库问题,还是在发指令(比如“重新加载文档”)。
  • 多轮对话管理:记住上下文,支持追问。
  • 引用格式化:把 RAGFlow 返回的引用信息整理成飞书消息卡片。
  • 异常兜底:RAGFlow 挂了或者超时了,给用户一个友好的提示,而不是沉默。

4.2 消息接收:lark-oapi 长连接客户端的正确用法

飞书 Python SDK 的长连接客户端用法如下:

import lark_oapi as lark from lark_oapi.api.im.v1 import * def do_message_receive(data: P2ImMessageReceiveV1) -> None: """处理接收到的消息""" message = data.event.message content = json.loads(message.content) user_text = content.get("text", "").strip() # 去掉 @ 机器人的部分 if message.mentions: for mention in message.mentions: user_text = user_text.replace(mention.key, "").strip() # 调用智能体处理 reply = agent_process(user_text, message.chat_id) # 回复消息 send_reply(message.chat_id, reply) # 创建长连接客户端 event_handler = lark.EventDispatcherHandler.builder("", "") \ .register_p2_im_message_receive_v1(do_message_receive) \ .build() cli = lark.ws.Client( app_id=APP_ID, app_secret=APP_SECRET, event_handler=event_handler, log_level=lark.LogLevel.INFO ) cli.start()

这里有个细节:register_p2_im_message_receive_v1注册的是单聊消息事件,群聊消息需要用register_p2_im_message_receive_v1配合群聊权限。另外,message.content是 JSON 字符串,需要json.loads解析,不同消息类型的 content 结构不一样,文本消息是{"text": "..."}。

4.3 调用 RAGFlow API:检索和对话两个接口的区别

RAGFlow 提供两类 API:检索接口和对话接口。

检索接口/api/v1/retrieval返回的是 chunk 列表,适合你想自己控制 prompt 的场景。对话接口/api/v1/chats/{chat_id}/completions是 RAGFlow 帮你把检索和生成都做了,直接返回答案。

我选的是检索接口 + 自己调大模型的方案,原因是:

  1. 我可以自己控制 prompt 模板,针对内部文档场景优化。
  2. 我可以换不同的大模型,不绑定 RAGFlow 内置的模型。
  3. 引用格式我可以自己定制,飞书消息卡片展示更灵活。

调用检索接口的代码大概长这样:

import requests def retrieve_from_ragflow(query: str, kb_id: str, top_n: int = 5): url = f"{RAGFLOW_BASE_URL}/api/v1/retrieval" headers = { "Authorization": f"Bearer {RAGFLOW_API_KEY}", "Content-Type": "application/json" } payload = { "question": query, "dataset_ids": [kb_id], "top_k": top_n, "similarity_threshold": 0.2, "vector_similarity_weight": 0.7 } resp = requests.post(url, json=payload, headers=headers, timeout=30) resp.raise_for_status() return resp.json().get("data", {}).get("chunks", [])

注意similarity_threshold这个参数,设太低会召回一堆无关内容,设太高会漏掉相关内容。我一般从 0.2 开始调,根据实际效果微调。

4.4 Prompt 设计:让大模型基于检索结果回答而不是瞎编

检索到 chunk 之后,拼 prompt 是关键。我的 prompt 模板大概是这样:

你是一个内部知识库助手。请根据以下参考资料回答用户问题。 规则: 1. 只根据参考资料回答,不要编造信息。 2. 如果参考资料中没有相关信息,直接说“知识库中没有找到相关内容”。 3. 回答要简洁,分点说明。 4. 在回答末尾列出引用的文档名称。 参考资料: {context} 用户问题:{question}

这个模板看起来简单,但每一条规则都是踩坑之后加的。比如“只根据参考资料回答”这条,不加的话大模型会用自己的知识补充,导致答案和内部文档不一致。“列出引用文档名称”这条,是为了让用户能溯源,增加信任感。

4.5 回复消息:飞书消息卡片比纯文本好用太多

飞书支持文本消息、富文本消息、消息卡片等多种格式。我一开始用纯文本,后来换成了消息卡片,原因是卡片可以:

  • 分区域展示:答案区、引用区、操作区。
  • 加按钮:比如“查看原文”“重新提问”。
  • 支持 Markdown 渲染:代码块、表格、列表都能正常显示。

消息卡片的 JSON 结构稍微复杂一点,但飞书有卡片搭建工具,可视化编辑之后导出 JSON 就行。我一般把答案放在div元素里,引用放在note元素里,操作按钮放在action元素里。

5. WebSocket 长连接的保活与重连:那些文档没写的事

5.1 心跳机制:多久发一次心跳才不会被断

飞书长连接底层是 WebSocket,WebSocket 连接如果长时间没有数据传输,中间的网络设备(负载均衡、防火墙)可能会主动断开。所以需要心跳机制——定期发一个 ping 帧,保持连接活跃。

飞书 SDK 内部已经封装了心跳,默认间隔是 30 秒。但我实测下来,某些网络环境下 30 秒还是会被断,后来我把心跳间隔调到了 15 秒,稳定了很多。如果你用的是自己实现的 WebSocket 客户端,心跳间隔建议设 10-20 秒。

5.2 断线重连:指数退避比固定间隔靠谱

长连接断开是常态,关键是断开之后怎么重连。我一开始用的是固定间隔重连,每 5 秒重试一次。结果发现如果服务端有问题,5 秒一次的重试会把日志刷爆,而且可能触发服务端的限流。

后来改成了指数退避:第一次断开等 1 秒重连,第二次等 2 秒,第三次等 4 秒,以此类推,最大不超过 60 秒。这样既保证了恢复速度,又避免了频繁重试。

import time import random def reconnect_with_backoff(max_retries=10): for attempt in range(max_retries): try: connect() return True except Exception as e: wait = min(2 ** attempt + random.uniform(0, 1), 60) print(f"重连失败,{wait:.1f}秒后重试: {e}") time.sleep(wait) return False

加随机抖动是为了避免多个客户端同时重连造成惊群效应。

5.3 消息去重:飞书可能重复推送同一条消息

这是一个很容易被忽略的坑:飞书在某些情况下会重复推送同一条消息,比如你的服务处理超时了,飞书没收到 ACK,就会重推。如果你的服务没有去重逻辑,用户就会收到两条一样的回复。

我的做法是用message_id做去重,维护一个最近处理过的 message_id 集合,处理之前先检查是否已经处理过。集合不用太大,保留最近 1000 条就够了,用collections.deque实现一个固定大小的队列。

5.4 并发处理:别让一条慢查询堵住所有消息

如果智能体服务是单线程处理消息,一条慢查询(比如 RAGFlow 检索超时 30 秒)会把后面所有消息都堵住。我的做法是用线程池或者异步任务队列来处理消息。

简单场景用concurrent.futures.ThreadPoolExecutor就够了:

from concurrent.futures import ThreadPoolExecutor executor = ThreadPoolExecutor(max_workers=4) def on_message(data): executor.submit(process_message, data)

但要注意,飞书长连接的回调函数如果返回太快,SDK 可能会认为处理完成。所以实际处理逻辑要放在线程池里,回调函数本身只负责提交任务。

6. 踩坑实录:那些让我加班到凌晨的问题

6.1 机器人收不到群聊消息:权限和 @ 的双重坑

第一个坑:机器人在群里收不到消息。排查过程:

  1. 先检查权限,发现im:message.group_msg没开。开了之后发布版本。
  2. 还是收不到。后来发现群聊里必须@ 机器人才会触发im.message.receive_v1事件。不 @ 的话,消息不会推给机器人。
  3. 但 @ 之后,message.content里的文本会包含@_user_1这样的 mention key,需要手动去掉。

这个坑的本质是:飞书的群聊消息事件设计就是只推 @ 机器人的消息,这是产品设计,不是 bug。所以机器人菜单和引导语里要明确告诉用户“请 @ 我提问”。

6.2 RAGFlow 检索返回空:相似度阈值设太高了

第二个坑:RAGFlow 检索经常返回空结果。排查过程:

  1. 先确认知识库里有文档,而且文档解析成功了。
  2. 直接调 RAGFlow 的检索接口,发现similarity_threshold默认是 0.5,太高了。
  3. 调到 0.2 之后,召回率明显提升。

但阈值也不是越低越好。设成 0 的话,什么乱七八糟的 chunk 都召回,大模型容易被误导。我的经验是0.15-0.25之间比较合适,具体看文档质量和查询类型。

6.3 大模型回答超时:流式输出 + 分段回复

第三个坑:大模型生成回答太慢,飞书消息回复有超时限制。飞书的被动回复消息有时间限制,超时了用户就看到“机器人无响应”。

解决方案有两个:

  • 方案一:先回一个“正在查询中...”的提示消息,然后等大模型生成完再发第二条消息。这需要用到飞书的主动发消息接口,而不是被动回复。
  • 方案二:用流式输出,大模型生成一段就发一段。但飞书消息不支持流式更新(除非用消息卡片的更新接口),实现起来比较复杂。

我选的是方案一,简单可靠。用户看到“正在查询中”就知道机器人在干活,不会以为挂了。

6.4 引用信息丢失:RAGFlow 返回的 chunk 里没有文档名

第四个坑:RAGFlow 检索返回的 chunk 里,document_name字段有时候是空的。排查发现是因为文档上传时没有正确设置元数据。

解决方案是在 RAGFlow 上传文档时,确保document_name和document_id都被正确记录。如果已经上传了,可以在 RAGFlow 的文档管理页面手动补充元数据。另外,检索接口返回的 chunk 里有一个doc_name字段,优先用这个。

6.5 内存泄漏:长连接跑几天之后内存暴涨

第五个坑:服务跑了两三天之后,内存从 200MB 涨到了 2GB。排查过程:

  1. 用tracemalloc抓内存快照,发现是消息去重的集合没有清理。
  2. 我一开始用的是普通set,只加不删,跑久了自然就爆了。
  3. 改成deque(maxlen=1000)之后,内存稳定了。

这个坑的教训是:任何只增不减的数据结构,在长驻服务里都是定时炸弹。日志、缓存、去重集合,都要有清理机制。

7. 实测效果与调优经验

7.1 响应时间:从 8 秒优化到 3 秒

整条链路的响应时间 = 飞书消息推送 + 智能体处理 + RAGFlow 检索 + 大模型生成 + 飞书消息发送。我实测下来,优化前平均 8 秒,优化后平均 3 秒。

优化点:

  • RAGFlow 检索:从 2 秒降到 0.8 秒,方法是把嵌入模型换成更小的模型,牺牲一点精度换速度。
  • 大模型生成:从 4 秒降到 1.5 秒,方法是换了一个更快的模型,并且限制了 max_tokens。
  • 飞书消息发送:从 1 秒降到 0.3 秒,方法是用了消息卡片而不是富文本,卡片渲染更快。

7.2 准确率:混合检索 + 重排序的效果

我做了个简单的评测,用 50 个内部常见问题测试,对比不同检索策略的准确率:

检索策略命中率回答准确率
纯向量检索72%65%
纯关键词检索58%52%
混合检索(0.7/0.3)86%78%
混合检索 + 重排序90%84%

重排序模型我用的 RAGFlow 内置的,效果比不用好一截。但重排序会增加 0.5-1 秒的延迟,需要权衡。

7.3 用户反馈:哪些问题问得最多

跑了一个月之后,我统计了一下用户问得最多的问题类型:

  • 流程类:“XX 流程怎么走”“XX 审批需要哪些材料”——占 40%
  • 接口类:“XX 接口的参数是什么”“XX 接口返回什么”——占 25%
  • 规范类:“XX 规范的要求是什么”——占 20%
  • 其他:占 15%

这个统计帮我优化了知识库的结构——流程类文档我单独建了一个知识库,解析时 chunk 设大一点;接口类文档单独建库,chunk 设小一点。分开之后,检索准确率又提升了一些。

8. 后续可以继续折腾的方向

这套链路跑通之后,我又陆续加了一些东西。比如多知识库路由——根据用户问题自动判断该查哪个知识库,而不是把所有文档塞在一个库里。实现方式是用一个小模型做意图分类,或者用关键词规则匹配。

还有对话历史管理——支持多轮追问。比如用户问“XX 流程怎么走”,机器人回答之后,用户接着问“那需要哪些材料”,机器人要能理解“那”指的是上一个问题。这个用 RAGFlow 的对话接口或者自己维护 session 都能实现。

另外反馈机制也很重要。我在消息卡片上加了个“有用/没用”的按钮,用户点了之后记录到数据库。积累一段时间之后,可以分析哪些问题回答得好、哪些回答得差,针对性优化知识库和 prompt。

最后说个小心得:别追求一步到位。我一开始想的是把所有功能都做完再上线,后来发现根本做不完。正确的做法是先跑通最小闭环——能收消息、能检索、能回复,然后让同事用起来,根据反馈迭代。上线第一周收到的反馈,比我闭门造车一个月都有价值。

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

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

立即咨询