Fork me on GitHub

2025年8月

实战 Coze Studio 智能体开发

昨天,我们成功在本地部署了 Coze Studio,并配置好了大模型。今天,我们将正式开始 Coze 智能体的探索之旅,学习如何利用其强大的可视化编排能力,创建并发布我们自己的 AI 智能体。

创建智能体

首先进入 “项目开发” 页面,点击 “创建” 按钮,Coze 支持创建两种不同的项目:

coze-create-agent.png

我们选择 “创建智能体”,然后为智能体取一个好听的名字,比如 “翻译小能手”:

coze-create-agent-name.png

接着,进入智能体的配置页面:

coze-create-agent-detail.png

整个配置页面可分为两大块:左侧为 编排 区域,右侧为 预览与调试 区域。在编排区域里,可以对智能体进行以下配置:

  • 人设与回复逻辑:指定模型的角色、设计回复的语言风格、限制模型的回答范围,让对话更符合用户预期;
  • 技能:为智能体配置各种扩展能力,让其可以调用外部插件或用户自定义工作流;
  • 知识:为智能体提供特定领域的知识背景,包括文本、表格和图片,让其可以回答领域内的问题;
  • 记忆:变量用来存储动态变化的信息,数据库用来管理和处理结构化数据,这两种方式都为智能体提供了一定的记忆能力;
  • 对话体验:开场白是用户进入智能体后自动展示的引导信息;用户问题建议用于在智能体回复后,根据对话内容推荐 3 条相关问题;快捷指令是对话输入框上方的按钮,方便用户快速发起预设对话;背景图片为智能体的对话窗口增加背景,增加对话沉浸感。

这些高级配置项我们暂时不管,对于 “翻译小能手”,我们只需要配置其人设与回复逻辑即可:

你是一个翻译助手,你的任务是将用户输入翻译成其他的语言,
如果用户输入是中文,翻译成英文,如果用户输入是英文,则翻译成中文。

一个简单的翻译智能体就制作完成了:

coze-translate-agent.png

另外,我们也可以打开 “探索” - “模版” 页面,Coze Studio 内置了两个智能体模版:英语聊天和导购陪练,都是纯提示词实现的,感兴趣的可以复制到工作空间看看:

coze-agent-template.png

自定义插件

单纯靠提示词实现的智能体能力非常有限,它无法连接和访问外部工具,我们可以为智能体添加一些技能,比如联网搜索、科学计算或绘制图片,扩展智能体的能力。上一节提到,在 Coze Studio 中,技能分为插件和工作流两种,下面我们先来学习下如何配置和使用插件。

这一节,我们将创建一个带技能的智能体,比如调用 “天气查询” 接口回答用户关于天气的问题。首先进入 “资源库” 页面,点击 “+ 资源” 按钮:

coze-add-resource.png

Coze 这里提供了五种不同类型的资源:

  • 插件:插件是一个工具集,一个插件内可以包含一个或多个工具,即 API 接口;
  • 工作流:通过在可视化画布上拖拽节点迅速搭建工作流,实现特定的业务逻辑;
  • 知识库:用户在这里上传外部知识内容,包括文本、表格和图片,解决大模型幻觉、专业领域知识不足的问题;
  • 提示词:可以将常用的提示词放在这里统一管理,在创建智能体或配置工作流中的大模型节点时可快速引用;
  • 数据库:管理和处理结构化数据,用户可通过自然语言插入、查询、修改或删除数据库中的数据;

我们这里选择创建插件:

coze-create-plugin.png

填写插件名称和描述,创建方式选 “云侧插件 - 基于已有服务创建”,插件 URL 填 https://query.asilu.com,该站点提供了不少免费而简单的 API 接口,用来测试智能体再合适不过。

注意插件的 URL 是根路径,插件下的所有工具都在这个 URL 之下。

Header 列表不用动,如果你的接口需要一些特别的请求头,可以在这里设置;授权方式选 “不需要授权”,除此之外,Coze Studio 还支持另两种授权方式:

  • Service 认证:通常是指一种简化的认证方式,其中 API 调用需要某种秘钥或令牌来验证其合法性,这种秘钥可能会通过查询参数或请求头传递;
  • OAuth 认证:这是一个开放标准,允许第三方应用在不共享用户密码的情况下访问用户账户的特定资源;

点击确定后进入插件页面,然后创建一个 get_weather 工具:

coze-create-plugin-tool.png

这个天气查询接口非常简单,只有一个 city 参数,如下:

因此我们需要配置三个地方:

  • 工具路径:/weather/baidu
  • 请求方法:Get 方法
  • 输入参数:只有一个 “city”,参数描述 “城市名”,参数类型 “String”,传入方法 “Query”

输出参数可以不用手工配,点击 “自动解析” 弹出一个调试页面,输入参数后只要接口能正常访问,Coze 会自动根据接口的返回值将输出参数配置好:

coze-create-plugin-tool-output.png

配置结束后,点击右上角的 “试运行” 对接口进行测试,测试完成后,这个插件就可以发布了。

然后,我们再创建一个 “天气助手” 智能体,将 get_weather 工具添加到它的技能里,这个智能体就可以为我们查询天气了:

coze-weather-agent.png

配置内置插件

其实,Coze Studio 提供了两种插件类型,即自定义插件和官方内置插件。官方内置插件由后台统一配置,支持所有用户使用;而自定义插件的使用范围为当前工作空间。可以打开 “探索” - “插件” 页面,查看所有官方内置插件:

coze-plugin-list.png

可以看到内置的插件还是蛮丰富的,一共 18 个,但是要注意的是,有些插件上带有 “未授权” 的标签,这表示此插件需要授权,这些插件我们还不能使用,需要配置授权信息。

所有内置的插件位于 backend/conf/plugin/pluginproduct 目录:

$ tree backend/conf/plugin/pluginproduct
backend/conf/plugin/pluginproduct
├── bocha_search.yaml # 博查搜索
├── chestnut_sign.yaml # 板栗看板
├── gaode_map.yaml # 高德地图
├── image_compression.yaml # 图片压缩
├── lark_authentication_authorization.yaml # 飞书认证及授权
├── lark_base.yaml # 飞书多维表格
├── lark_calendar.yaml # 飞书日历
├── lark_docx.yaml # 飞书云文档
├── lark_message.yaml # 飞书消息
├── lark_sheet.yaml # 飞书电子表格
├── lark_task.yaml # 飞书任务
├── lark_wiki.yaml # 飞书知识库
├── library_search.yaml # 文库搜索
├── maker_smart_design.yaml # 创客贴智能设计
├── plugin_meta.yaml # 所有插件的元配置,包括授权信息
├── sky_eye_check.yaml # 天眼查
├── sohu_hot_news.yaml # 搜狐热闻
├── wolfram_alpha.yaml # Wolfram Alpha
└── worth_buying.yaml # 什么值得买

其中 plugin_meta.yaml 为所有插件的元配置,授权信息就是配在这个文件中。我们以 “博查搜索” 为例,在该文件中找到对应的配置块:

coze-plugin-meta.png

根据提示,访问 博查 AI 开放平台

bocha-home.png

博查搜索是国内为数不多的一家搜索服务提供商,虽然不是免费,但价格确实很便宜,一次调用仅 3 分钱,为国外同类产品的 1/3。点击 “控制台” 注册、充值、并创建 API KEY,将其配置在 payload 中的 service_token 字段:

  auth:
    type: service_http
    key: Authorization
    sub_type: token/api_key
    # service token apply to https://open.bochaai.com/
    payload: '{"key": "Authorization", "service_token": "sk-xxx", "location": "Header"}'

这里的 auth.type 就对应上一节我们创建插件时选择的 “授权方式”,支持 service_httpoauth 两种,具体的授权信息位于 payload 里。关于插件配置和授权配置的详细解释,可以参考官方文档:

配置好插件授权后,重启 Coze 服务:

$ docker compose --profile "*" restart coze-server

此时 “博查搜索” 插件就可以正常使用了。我们可以创建一个 “搜索专家” 智能体,让它调用搜索引擎来回答用户的问题:

coze-search-agent.png

小结

今天,我们正式开启了 Coze Studio 的智能体开发之旅。我们从最基础的提示词工程开始,创建了一个简单的 “翻译小能手” 智能体,体验了仅通过人设与回复逻辑就能快速构建应用的过程。

接着,我们深入学习了如何通过插件扩展智能体的能力。我们一步步创建自定义的 “天气查询” 插件,并通过它实现了 “天气助手” 智能体;另外,还学习了如何配置和使用 Coze Studio 内置的插件,然后使用 “博查搜索” 插件,让智能体能够连接互联网获取实时信息。

通过今天的实战,我们掌握了 Coze Studio 中智能体和插件的基本用法,在后面的学习中,我们将继续探索工作流、知识库等更高级的功能,敬请期待。


Coze Studio 快速上手指南

相信大家最近都被 Coze 开源的新闻刷屏了吧,作为国内智能体平台的扛把子,字节的这一波操作确实让人猝不及防。Coze 是国内最早一批做智能体平台的,它的很多功能做得确实很不错,用户体验也很赞;但是,随着今年深度搜索和深度研究等概念的兴起,智能体平台的定位有点尴尬,字节本身也把精力投到 扣子空间 这个产品上了,这估计也是字节选择开源 Coze 的一个原因吧。

本次开源包括 Coze Studio(扣子开发平台)Coze Loop(扣子罗盘) 两个核心项目,并采用了 Apache 2.0 许可协议,这意味着开发者可以自由修改甚至闭源商用,这让那些其他的智能体开发平台,比如 Dify,瞬间就不香了。

我也算是 Coze 的老用户了,一直关注着它的最新动态,只要一有新功能推出,我总是第一时间去体验,然后和小伙伴们讨论,猜猜它后端可能是怎么实现的。现在开源了,这不得好好研究下它的代码嘛。

Coze Studio 介绍

根据官网的介绍,Coze Studio 源自 扣子开发平台,是一个一站式 AI 智能体开发平台,通过 Coze Studio 提供的可视化设计与编排工具,开发者可以通过零代码或低代码的方式,快速打造和调试智能体、应用和工作流,实现强大的 AI 应用开发和更多定制化业务逻辑。

coze-logo.png

它的核心功能包括:

  • 模型服务:管理模型列表,可接入 OpenAI、火山方舟等在线或离线模型服务;
  • 搭建智能体:编排、发布、管理智能体,支持配置工作流、知识库等资源;
  • 搭建应用:创建、发布应用,通过工作流搭建业务逻辑;
  • 搭建工作流:创建、修改、发布、删除工作流;
  • 开发资源:支持创建并管理以下资源:插件、知识库、数据库、提示词;
  • API 与 SDK:创建会话、发起对话等 OpenAPI,通过 Chat SDK 将智能体或应用集成到自己的应用;

本地部署

Coze 提供了项目所需的所有镜像,使用 Docker Compose 可以快速进行部署。我们首先克隆源码:

$ git clone https://github.com/coze-dev/coze-studio.git

进入 docker 目录:

$ cd coze-studio/docker

这个目录下的 docker-compose.yml 文件定义了部署 Coze 包含的各个组件,包括:

  • coze-mysql - MySQL 结构化数据存储
  • coze-redis - Redis 缓存
  • coze-elasticsearch - Elasticsearch 存储
  • coze-minio - Minio 对象存储
  • coze-milvus - Milvus 向量数据库
  • coze-etcd - Milvus 依赖 etcd 管理元数据
  • coze-server - Coze 后端服务

Coze 默认使用 NSQ 作为消息中间件服务,这是一个基于 Go 语言编写的内存分布式消息中间件,包括下面三个组件:

  • coze-nsqd - 负责接收、排队和向客户端投递消息,处理消息收发和队列维护的组件
  • coze-nsqlookupd - 管理拓扑信息的守护进程,相当于中心管理服务和服务发现组件
  • coze-nsqadmin - 一个 Web UI,用于实时查看聚合的集群统计信息,并执行各种管理任务

此外,有些组件在部署时需要初始化一些数据,Coze 通过下面这三个组件来初始化:

  • coze-minio-setup - 导入图标类的资源文件
  • coze-mysql-setup-schema - 初始化 MySQL 表结构,使用 Atlas 工具根据 HCL 文件创建数据库表结构
  • coze-mysql-setup-init-sql - 初始化 MySQL 表结构,导入初始化数据,和 coze-mysql-setup-schema 的区别是,这个服务使用 MySQL 原生客户端执行 SQL 脚本文件

为什么有两个初始化 MySQL 的服务?可能是为了兼容不同的部署方式,或者是逐步从传统 SQL 到 Atlas 迁移升级。

这个目录下还有一个 .env.example 文件,里面包含大量的项目配置,我们需要将其复制一份出来,另存为 .env 文件:

$ cp .env.example .env

如果你希望修改数据库用户名或密码之类的,可以编辑这个文件,默认情况下不用动。直接 docker compose 启动即可:

$ docker compose --profile "*" up -d

等待所有容器启动完毕:

coze-docker-up.png

其中 coze-minio-setupcoze-mysql-setup-schemacoze-mysql-setup-init-sql 这几个容器完成初始化任务后就退出了,因此处于 Exited 状态,是正常现象。

如果一切顺利,通过浏览器访问 http://localhost:8888/ 即可进入 Coze Studio 页面:

coze-login.png

输入邮箱和密码,点击注册,进入工作空间:

coze-space.png

模型配置

不过这个时候我们还无法使用平台功能,比如创建智能体,会报如下错误:

coze-create-agent-fail.png

Coze Studio 是一款基于大模型的 AI 应用开发平台,因此我们还必须配置模型服务。模型配置为 YAML 文件,统一放在 backend/conf/model 目录中,可以存在多个,每个文件对应一个可访问的模型。Coze Studio 支持常见的模型服务,如 OpenAI、DeepSeek、豆包等,为方便开发者快速配置,Coze Studio 在 backend/conf/model/template 目录下提供了常见模型的模板文件:

$ tree backend/conf/model/template 
backend/conf/model/template
├── model_template_ark.yaml
├── model_template_ark_doubao-1.5-lite.yaml
├── model_template_ark_doubao-1.5-pro-256k.yaml
├── model_template_ark_doubao-1.5-pro-32k.yaml
├── model_template_ark_doubao-1.5-thinking-pro.yaml
├── model_template_ark_doubao-1.5-thinking-vision-pro.yaml
├── model_template_ark_doubao-1.5-vision-lite.yaml
├── model_template_ark_doubao-1.5-vision-pro.yaml
├── model_template_ark_doubao-seed-1.6-flash.yaml
├── model_template_ark_doubao-seed-1.6-thinking.yaml
├── model_template_ark_doubao-seed-1.6.yaml
├── model_template_ark_volc_deepseek-r1.yaml
├── model_template_ark_volc_deepseek-v3.yaml
├── model_template_basic.yaml
├── model_template_claude.yaml
├── model_template_deepseek.yaml
├── model_template_gemini.yaml
├── model_template_ollama.yaml
├── model_template_openai.yaml
└── model_template_qwen.yaml

可以看到,除了字节自家的豆包(ARK 表示 火山方舟,豆包系列的大模型都支持),Coze Studio 也内置了 OpenAI、DeepSeek、Claude、Ollama、Qwen、Gemini 等模型的支持。

最近,国产开源模型大爆发,从月之暗面的 Kimi K2、阿里的 Qwen3 到智谱的 GLM 4.5,模型效果一个比一个好。这些模型在魔搭也都上线了:

modelscope.jpg

其中,Qwen3 和 GLM 4.5 都提供了推理 API 可以免费调用,每天 2000 次额度,我们不妨用 Qwen3 来测试一下。魔搭的推理 API 兼容 OpenAI 接口协议,因此我们这里使用 OpenAI 模版,将其复制到 backend/conf/model 目录:

$ cp backend/conf/model/template/model_template_openai.yaml backend/conf/model/model_modelscope_qwen3_coder.yaml

模版文件对各个参数已经有了比较详细的解释,一般来说,大多数参数都不用动,只需要关注其中几个重要参数即可:

  • id - 模型 ID,由开发者自行定义,必须是非 0 的整数,且全局唯一,模型 ID 定下来之后最好就不要改了;
  • name - 模型在平台上展示的名称;
  • description - 模型在平台上展示的简介,分中英文;
  • default_parameters - 模型默认参数,包括 temperaturemax_tokenstop_pfrequency_penaltypresence_penaltyresponse_format 等,基本上不用动;
  • meta.capability - 模型具备的能力,根据实际情况配置,比如是否支持 function call,是否支持 json mode,是否支持 reasoning,是否支持多模态,等;
  • meta.conn_config.base_url - 模型服务的接口地址,如果使用的不是 OpenAI 官方接口,可以在这里配置;比如这里我使用魔搭的推理 API 接口;
  • meta.conn_config.api_key - 模型服务的 API Key;注册魔搭平台后,在 “账号设置” - “访问令牌” 页面创建;
  • meta.conn_config.model - 模型名,不同厂商的命名规则可能不一样;比如这里我使用 Qwen3-Coder 来测试,在魔搭上的模型名为 Qwen/Qwen3-Coder-480B-A35B-Instruct
  • meta.conn_config.openai - OpenAI 专属配置,这里将 by_azure 设置为 false

更多参数介绍,请参考官方的 模型配置文档

修改后的配置文件内容如下:

coze-model-conf.png

然后执行以下命令重启 Coze 服务,使配置生效:

$ docker compose --profile "*" restart coze-server

再次点击创建智能体,此时就可以成功创建了,模型下拉列表可以看到我们配置的模型服务:

coze-create-agent-model.png

在右边的 “预览与调试” 对话框聊上两句,测试下模型服务是否正常:

coze-create-agent-success.png

小结

本文作为 Coze Studio 的快速上手指南,我们首先介绍了 Coze 开源的背景及其核心功能,然后详细讲解了如何使用 Docker Compose 在本地环境中完成部署,并以 OpenAI 兼容模型为例,演示了如何配置和验证模型服务。通过这些步骤,我们成功搭建了一个可用的 Coze Studio 本地开发环境。

至此,一切准备就绪,让我们一起开始 Coze 智能体的探索之旅吧!


再学 RAGFlow 的问答流程(二)

废话不多说,我们今天继续学习昨天遗留的几个问答过程中的细节问题。

上下文管理

RAGFlow 检索完知识库后,需要将检索结果和系统提示词拼接丢给大模型回答问题,此时我们面临一个问题,检索的内容可能很长,超出大模型的最大 token 数,导致调用报错。RAGFlow 使用 message_fit_in() 函数对上下文的 token 进行管理,确保会话消息不超过模型限制的 95%:

def chat(dialog, messages, stream=True, **kwargs):
  # ...
  max_tokens = llm_model_config.get("max_tokens", 8192)
  used_token_count, msg = message_fit_in(msg, int(max_tokens * 0.95))

这里的 max_tokens 表示大模型的最大 token 数,我们之前在 “模型提供商” 页面添加模型时,可能已经注意到了:

ragflow-add-model.png

对于一些常见的模型,我们可以不用填写,RAGFlow 内置了这个值,可以参考 conf/llm_factories.json 文件:

{
  "factory_llm_infos": [
    {
      "name": "OpenAI",
      "logo": "",
      "tags": "LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,MODERATION",
      "status": "1",
      "llm": [
        {
          "llm_name": "gpt-4.1",
          "tags": "LLM,CHAT,1M,IMAGE2TEXT",
          "max_tokens": 1047576,
          "model_type": "chat",
          "is_tools": true
        },
        {
          "llm_name": "gpt-4.1-mini",
          "tags": "LLM,CHAT,1M,IMAGE2TEXT",
          "max_tokens": 1047576,
          "model_type": "chat",
          "is_tools": true
        },
        // ...
      ]
    },
    // ...
  ]
}

下面我们来看下 message_fit_in() 函数的逻辑:

def message_fit_in(msg, max_length=4000):

  # 计算当前 msg 中所有文本的 token 数量
  def count():
    nonlocal msg
    tks_cnts = []
    for m in msg:
      tks_cnts.append({"role": m["role"], "count": num_tokens_from_string(m["content"])})
    total = 0
    for m in tks_cnts:
      total += m["count"]
    return total

  # 如果总 token 数未超限,直接返回原始消息
  c = count()
  if c < max_length:
    return c, msg

  # 保留所有 system 角色消息(也就是系统提示词)
  msg_ = [m for m in msg if m["role"] == "system"]
  if len(msg) > 1:
    # 保留最后一条消息(通常是用户的最新问题),丢弃中间的会话历史
    msg_.append(msg[-1])
  msg = msg_
  c = count()
  if c < max_length:
    return c, msg

  # 系统消息 token 数
  ll = num_tokens_from_string(msg_[0]["content"])
  # 最后消息 token 数
  ll2 = num_tokens_from_string(msg_[-1]["content"])
  # 如果系统消息占比超过 80%
  if ll / (ll + ll2) > 0.8:
    # 截断系统消息
    m = msg_[0]["content"]
    m = encoder.decode(encoder.encode(m)[: max_length - ll2])
    msg[0]["content"] = m
    return max_length, msg
  # 否则截断最后一条消息
  # 这里貌似有个 BUG,应该是 `max_length - ll`
  m = msg_[-1]["content"]
  m = encoder.decode(encoder.encode(m)[: max_length - ll2])
  msg[-1]["content"] = m
  return max_length, msg

其中 count() 用于计算当前 msg 中所有文本的 token 数量,它使用 num_tokens_from_string() 函数计算每条消息的 token 数,它是基于 tiktokencl100k_base 编码器实现的(GPT-3.5/GPT-4标准):

import tiktoken

def num_tokens_from_string(string: str) -> int:
  encoder = tiktoken.get_encoding("cl100k_base")
  return len(encoder.encode(string))

message_fit_in() 函数通过三级截断策略,确保当前 msg 不超出模型的最大 token 数:

  • 第一级,无需处理:如果总 token 数未超限,直接返回原始消息;
  • 第二级,保留系统消息+最后一条消息:保留所有 system 角色消息(也就是系统提示词),保留最后一条消息(通常是用户的最新问题),丢弃中间的会话历史;
  • 第三级:智能内容截断:如果系统消息占比超过 80% 则截断系统消息,否则截断最后一条消息;

可以看到,RAGFlow 的截断策略是 system 消息优先级最高,因为这里包含重要的系统提示和知识,历史会话可以牺牲,只保留用户最新消息;不过这里还可以再细化一点,比如保留最近几轮会话,而不是直接丢弃所有历史会话,只要不超出总数都可以。

引用处理

生成有理有据的回答是 RAGFlow 的核心特性之一,它支持在生成的回答中附带关键引用的快照,并支持追根溯源,最大限度地减少了 AI 幻觉。RAGFlow 在处理引用时别出心裁,综合使用 大模型文本块语义匹配 两种手段在生成的回答中插入引用。首先看下大模型的使用:

def chat(dialog, messages, stream=True, **kwargs):
  # ...
  # 开启引用的提示词
  if knowledges and (prompt_config.get("quote", True) and kwargs.get("quote", True)):
    prompt4citation = citation_prompt()

  # 调用大模型流式输出
  for ans in chat_mdl.chat_streamly(prompt + prompt4citation, msg[1:], gen_conf):
    yield {"answer": thought + answer, "reference": {}, "audio_binary": tts(tts_mdl, delta_ans)}
  yield decorate_answer(thought + answer)

主要是在系统提示词后面追加一段对引用的要求:

## 引用要求

- 采用统一的引用格式,例如[ID:i] [ID:j],其中“i”和“j”为置于方括号中的文档ID。多个ID之间用空格分隔(例如,[ID:0] [ID:1])。
- 引用标记必须放在句尾,与最后的标点符号(如句号、问号)之间用空格隔开。
- 每个句子最多允许使用4个引用。
- 如果内容并非来自检索到的片段,请勿插入引用。
- 请勿使用独立的文档ID(例如,#ID#)。
- 引用必须始终遵循[ID:i]格式。
- 严禁使用删除线符号(例如,~~)或任何其他非标准格式语法。
- 任何违反上述规则的行为——包括格式错误、使用禁止的样式或不支持的引用方式——都将导致该句子不添加任何引用。

RAGFlow 将知识库检索片段添加到系统提示词时,会为每一段分配一个 ID 序号:

ragflow-doc-id.png

如果大模型能准确理解引用要求,就会在对应的句子后面加上引用标记,RAGFlow 在大模型输出结束后,会调用 decorate_answer() 函数对引用标记进行提取和解析,然后追加一帧完整的结果,包括引用的文档信息、使用的提示词、详细的时间统计等:

def decorate_answer(answer):
  # ...
  if knowledges and (prompt_config.get("quote", True) and kwargs.get("quote", True)):
    idx = set([])
    if embd_mdl and not re.search(r"\[ID:([0-9]+)\]", answer):
      # 大模型返回的答案中没有引用,使用语义匹配的方式生成引用
      answer, idx = retriever.insert_citations(answer, ...)
    else:
      # 大模型返回的答案中有引用,提取出所有的引用序号
      for match in re.finditer(r"\[ID:([0-9]+)\]", answer):
        i = int(match.group(1))
        if i < len(kbinfos["chunks"]):
          idx.add(i)
    # 修复错误引用格式
    answer, idx = repair_bad_citation_formats(answer, kbinfos, idx)
  # 返回完整结果,包括引用,提示词,详细时间统计等
  return {"answer": think + answer, "reference": refs, "prompt": re.sub(r"\n", "  \n", prompt), "created_at": time.time()}

这里比较有意思的是 RAGFlow 对大模型返回的答案中没有引用的情况的处理,它会调用 insert_citations() 函数使用语义匹配的方式生成引用,具体的处理流程大致如下:

  1. 答案分段:将答案按句号、问号、感叹号等标点符号分割成片段;
  2. 向量编码:使用嵌入模型对答案片段进行编码,确保答案向量和文档块向量维度一致;
  3. 相似度计算:对每个答案片段,计算与所有文档块的混合相似度(加权向量相似度和关键词相似度)。这里的处理也挺有意思的,采用了动态阈值来筛选,初始阈值为 0.63,使用该阈值筛选高相似度的匹配,如果没有匹配,逐步降低阈值重试(每次乘以 0.8),阈值最低不低于 0.3;
  4. 引用插入:在相似度超过阈值的答案片段后插入引用标记。

最后再通过 repair_bad_citation_formats() 函数修复答案中不规范的引用格式,将各种错误的引用格式统一转换为标准的 [ID:数字] 格式。函数通过 BAD_CITATION_PATTERNS 定义的正则表达式模式来识别错误格式:

BAD_CITATION_PATTERNS = [
  re.compile(r"\(\s*ID\s*[: ]*\s*(\d+)\s*\)"),  # (ID: 12) - 圆括号格式
  re.compile(r"\[\s*ID\s*[: ]*\s*(\d+)\s*\]"),  # [ID: 12] - 方括号但带空格的格式
  re.compile(r"【\s*ID\s*[: ]*\s*(\d+)\s*】"),  # 【ID: 12】 - 中文方括号格式
  re.compile(r"ref\s*(\d+)", flags=re.IGNORECASE),  # ref12、REF 12 - ref 引用格式(不区分大小写)
]

深度推理模式

从 v0.17.0 起,RAGFlow 支持在 AI 聊天中集成深度推理模式(Agentic RAG),该模式模仿了人类研究员的工作方式:将一个大问题分解为多个子问题,分别对每个子问题进行深入研究(检索),然后将所有研究结果汇总,最终形成一个全面而有深度的答案:

ragflow-deep-search-flow.png

深度推理的核心逻辑位于 agentic_reasoning/deep_research.py 文件中:

class DeepResearcher:
  # ...
  def thinking(self, chunk_info: dict, question: str):
    
    think = "<think>"
    for step_index in range(MAX_SEARCH_LIMIT + 1):

      # 大模型推理,判断是否需要进一步搜索
      query_think = ""
      for ans in self._generate_reasoning(msg_history):
        query_think = ans
        yield {"answer": think + self._remove_query_tags(query_think) + "</think>", "reference": {}, "audio_binary": None}
      think += self._remove_query_tags(query_think)
      
      # 提取搜索问题
      queries = self._extract_search_queries(query_think, question, step_index)
      if not queries and step_index > 0:
        # 不需要继续搜索,退出循环
        break

      # 每个问题单独处理
      for search_query in queries:

        # 开始处理 ...
        think += f"\n\n> {step_index + 1}. {search_query}\n\n"
        yield {"answer": think + "</think>", "reference": {}, "audio_binary": None}
        
        # 检索:知识库、搜索引擎、知识图谱
        kbinfos = self._retrieve_information(search_query)
        
        # 从检索结果中提取关于该问题的关键信息
        think += "\n\n"
        summary_think = ""
        for ans in self._extract_relevant_info(truncated_prev_reasoning, search_query, kbinfos):
          summary_think = ans
          yield {"answer": think + self._remove_result_tags(summary_think) + "</think>", "reference": {}, "audio_binary": None}
        think += self._remove_result_tags(summary_think)

    # 返回所有的 think 步骤
    yield think + "</think>"

可以看出深度推理的本质就是一个 for 循环,通过不断的调用大模型,判断当前还缺失哪些信息,从而生成需要进一步搜索的问题,然后通过检索知识库、搜索引擎或知识图谱等补充对应的信息,直到大模型认为所有信息都已经完整为止。整个流程可以分为三个步骤:

  • 生成推理: 调用 _generate_reasoning 生成推理步骤,让大模型判断是否需要搜索,如果需要,则按特定格式输出要搜索的问题;
  • 提取问题:调用 _extract_search_queries 从大模型输出中提取搜索问题,如果为空,说明不需要继续搜索,则退出循环;
  • 检索问题:针对每个问题分别调用 _retrieve_information 进行检索,支持知识库、搜索引擎(需要配置 Tavily API KEY)、知识图谱(需要开启知识图谱功能)三种检索方式;
  • 信息提取:为防止多轮检索结果过多对上下文造成干扰,每次检索后,通过调用 _extract_relevant_info 从检索结果中提取关于当前问题的关键信息;

生成推理和信息提取基于大模型实现,提示词都比较简单,可参考源码。

每一轮提取的关键信息都会拼接到 think 字段中,最后返回完整的思考过程,这个内容将作为知识库喂给大模型,以便它基于该内容回答用户问题:

def chat(dialog, messages, stream=True, **kwargs):
  # ...
  if prompt_config.get("reasoning", False):
    reasoner = DeepResearcher(chat_mdl, prompt_config, ...)
    for think in reasoner.thinking(kbinfos, " ".join(questions)):
      # 返回的是字符串,表示完整的 think 过程
      if isinstance(think, str):
        thought = think
        # 将其按换行分割作为知识库内容,大模型基于该内容回答用户问题
        knowledges = [t for t in think.split("\n") if t]
      # 返回的不是字符串,直接流式输出
      elif stream:
        yield think

下面是深度推理的一个示例,可以看到复杂问题被拆成了两步检索:

ragflow-deep-search.png

通过这种分而治之再综合归纳的策略,深度推理模式能够处理普通 RAG 难以应对的复杂查询,提供更高质量、更有深度的答案。

小结

今天我们继续深入探讨了 RAGFlow 问答流程中的几个核心技术细节:

  • 上下文管理:学习了 RAGFlow 如何通过智能的三级截断策略来管理上下文长度,确保即使在复杂的对话中也不会超出模型的 Token 限制;
  • 引用处理:分析了其独特的引用处理机制,它结合大模型和语义匹配,不仅能生成带引用的回答,还能自动修复格式错误,保证了答案的可追溯性;
  • 深度推理模式:揭示了深度推理模式的奥秘,了解了 RAGFlow 如何模仿人类研究员,通过多轮检索和归纳,解决复杂的多跳问题;

至此,我们已经贯穿了 RAGFlow 从知识库构建到最终问答的完整生命周期。通过这一系列的学习,我们不仅掌握了 RAGFlow 的使用方法,更深入理解了其背后的设计哲学和技术实现。希望这些内容能帮助你更好地利用 RAGFlow 构建出强大、智能的 RAG 应用。