乐于分享
好东西不私藏

给 RAGFlow Agent 写插件:从 Mock 到生产的全链路

给 RAGFlow Agent 写插件:从 Mock 到生产的全链路

AI 客服最核心的价值不是”能聊天”,而是”能办事”——查订单、追物流,这需要给 Agent 接入真实业务 API。但开发阶段没有真实接口怎么办?Mock 先行,生产无缝切换。

本文以 RAGFlow 平台为例,分享从插件开发、Mock 数据设计、单号格式识别,到生产环境切换的完整工程实践。

一、RAGFlow 插件机制概述

RAGFlow 的 Agent 节点可以绑定”工具”(Tool),工具就是一个 Python 类,放在 agent/tools/ 目录下,运行时被 Agent 调用。

插件的三个核心要素

from agent.tools.base import ToolParamBase, ToolBase, ToolMeta
1. 参数定义:声明工具的入参、类型、描述

class EcoflowOrderQueryParam(ToolParamBase):
def init(self):
self.meta: ToolMeta = {
“name”: “ecoflow_order_query”,
“description”: “Query EcoFlow order status by order number…”,
“parameters”: {
“order_number”: {
“type”: “string”,
“description”: “The order number to query”,
“required”: True,
},
“platform”: {
“type”: “string”,
“description”: “Platform type: EcoFlow, Amazon, or eBay”,
“enum”: [“EcoFlow”, “Amazon”, “eBay”],
“default”: “EcoFlow”,
“required”: False,
},
},
}

2. 工具类:实现执行逻辑

class EcoflowOrderQuery(ToolBase, ABC):
component_name = “EcoflowOrderQuery”

这个名字就是 Agent 绑定时的引用

@timeout(15)
def _invoke(self, **kwargs):

业务逻辑

result = {“orderNumber”: “…”, “status”: “SHIPPED”, …}
self.set_output(“json”, result)
return json.dumps(result, ensure_ascii=False)


**关键点**:

- `component_name`:全局唯一标识,Agent 节点的 `tools` 数组通过这个名字绑定
- `meta.description`:LLM 读这段描述来决定是否调用工具,写得越精确越好
- `meta.parameters`:参数定义决定了 LLM 传什么参数进来,`required` 标记必填项
二、双模式设计:Mock / 真实 API

环境变量驱动切换

MOCK_MODE = os.environ.get("ECOFLOW_MOCK_MODE", "true").lower() == "true"
THIRD_PARTY_API_BASE_URL = os.environ.get("ECOFLOW_API_BASE_URL", "")
API_KEY = os.environ.get("ECOFLOW_API_KEY", "")

三个环境变量,默认值都是安全的选择:

环境变量 默认值 说明
ECOFLOW_MOCK_MODE true 默认 Mock,防止开发阶段误调真实 API
ECOFLOW_API_BASE_URL "" 空 = 不调用外部接口
ECOFLOW_API_KEY "" 空 = 无认证

运行时切换逻辑

try:
    if MOCK_MODE:
        result = MOCK_ORDERS.get(order_number, {"error": "ORDER_NOT_FOUND", ...})
    else:
        import requests
        resp = requests.post(
            f"{THIRD_PARTY_API_BASE_URL}/api/order/query",
            json={"orderNumber": order_number, "platform": platform},
            headers={"Authorization": f"Bearer {API_KEY}"},
            timeout=10,
        )
        result = resp.json()
except Exception as e:
    result = {"error": "QUERY_FAILED", "message": str(e)}

设计意图

  • Mock 模式下 requests 库不会被 import,减少依赖和启动时间
  • 异常统一捕获,返回结构化错误信息,Agent 能理解并转达给用户
  • MOCK_MODE 默认为 true,即使环境变量没配也不怕——宁可查不到数据,不能调错接口
三、订单查询插件实战

订单号格式自动识别

EcoFlow 的订单来自多个平台,订单号格式各不相同:

ORDER_PATTERNS = {
    "EcoFlow":  re.compile(r"EF[A-Z]{2}-\d{4,10}", re.IGNORECASE),
EFUS-121453

“Amazon”:   re.compile(r”\d{3}-\d{7}-\d{7}”),

113-1234567-1234567

“eBay”:     re.compile(r”\d{2}-\d{5}-\d{5}”),

12-12345-12345

}


自动识别逻辑:

```python
if not platform:
    for plat, pattern in ORDER_PATTERNS.items():
        if pattern.search(order_number):
            platform = plat
            break
    if not platform:
        platform = "EcoFlow"
兜底默认

**为什么需要自动识别?** 用户输入时不会特意标注"这是 Amazon 订单"——他们只会丢一个订单号过来。自动识别让交互更自然。

**Mock 数据设计**

Mock 数据要覆盖正常 + 异常 + 边界场景:

```python
MOCK_ORDERS = {
正常:已发货(含快递单号,可级联查物流)

“EFUS-121453”: {
“orderNumber”: “EFUS-121453”,
“status”: “SHIPPED”,
“items”: [{“productName”: “DELTA 3 Plus”, “quantity”: 1}],
“trackingNumber”: “SF1234567890”,
“carrier”: “SF Express”,
“estimatedDelivery”: “2026-04-25”,
},

正常:已送达

“EFUS-121454”: {
“orderNumber”: “EFUS-121454”,
“status”: “DELIVERED”,
“items”: [{“productName”: “RIVER 3”, “quantity”: 1}],
“trackingNumber”: “1Z999AA10123456784”,
“carrier”: “UPS”,
“deliveredAt”: “2026-04-10”,
},

正常:处理中(无快递单号,不能查物流)

“EFUS-121455”: {
“orderNumber”: “EFUS-121455”,
“status”: “PROCESSING”,
“items”: [
{“productName”: “DELTA 3 Plus”, “quantity”: 1},
{“productName”: “Extra Battery”, “quantity”: 2},
],
},

异常:已取消

“EFDE-789012”: {
“orderNumber”: “EFDE-789012”,
“status”: “CANCELLED”,
“cancelReason”: “Customer requested cancellation”,
},

跨平台:Amazon 订单

“113-1234567-1234567”: {
“orderNumber”: “113-1234567-1234567”,
“status”: “SHIPPED”,
“trackingNumber”: “TBA123456789000”,
“carrier”: “Amazon Logistics”,
},

跨平台:eBay 订单

“12-12345-12345”: {
“orderNumber”: “12-12345-12345”,
“status”: “SHIPPED”,
“trackingNumber”: “9400111899223456789012”,
“carrier”: “USPS”,
},
}


**6 条 Mock 数据的设计逻辑**:

| 场景 | 数据 | 测试价值 |
|------|------|---------|
| 已发货 | EFUS-121453 | 正常流程 + 级联查物流 |
| 已送达 | EFUS-121454 | 终态展示 |
| 处理中 | EFUS-121455 | 无快递单号的场景 |
| 已取消 | EFDE-789012 | 异常状态展示 |
| Amazon | 113-... | 跨平台格式识别 |
| eBay | 12-... | 跨平台格式识别 |

对于不存在的订单号,返回统一结构:

```python
{"error": "ORDER_NOT_FOUND", "message": f"Order {order_number} not found."}
四、物流查询插件实战

运营商单号前缀识别

物流查询的难点在于:用户给一个快递单号,需要先判断是哪家运营商,再查对应的系统。

CARRIER_PATTERNS = [
    ("SF Express",       re.compile(r"^SF\d{10,15}${paragraph}quot;, re.IGNORECASE)),
SF1234567890

(“UPS”,              re.compile(r”^1Z[0-9A-Z]{16}${paragraph}quot;, re.IGNORECASE)),

1Z999AA10123456784

(“Amazon Logistics”, re.compile(r”^TBA\d{10,15}${paragraph}quot;, re.IGNORECASE)),

TBA123456789000

(“USPS”,             re.compile(r”^9[234]\d{18,22}${paragraph}quot;)),

9400111899223456789012

(“DHL”,              re.compile(r”^(JD|JJD)\d{10,18}${paragraph}quot;, re.IGNORECASE)),

JD0012345678901

(“FedEx”,            re.compile(r”^\d{12,22}${paragraph}quot;)),

兜底长数字

]


**注意**:FedEx 的正则是兜底型(纯长数字),放在最后匹配。因为 FedEx 单号没有独特前缀,只能靠排除法。

**物流轨迹的 Mock 数据设计**

物流查询的返回比订单复杂——需要一条时间线:

```python
"SF1234567890": {
    "trackingNumber": "SF1234567890",
    "carrier": "SF Express",
    "status": "IN_TRANSIT",
    "estimatedDelivery": "2026-04-25",
    "events": [
        {"time": "2026-04-17T14:30:00Z", "location": "San Francisco, CA",
         "description": "Out for delivery"},
        {"time": "2026-04-16T08:00:00Z", "location": "Los Angeles, CA",
         "description": "Arrived at distribution center"},
        {"time": "2026-04-15T14:00:00Z", "location": "Shenzhen, CN",
         "description": "Departed origin facility"},
        {"time": "2026-04-14T10:00:00Z", "location": "Shenzhen, CN",
         "description": "Shipment picked up"},
    ],
}

设计要点

  • events 按时间倒序排列(最新的在前),Agent 可以只展示最近 2-3 条
  • 覆盖 3 种物流状态:IN_TRANSIT(运输中)、DELIVERED(已送达)、EXCEPTION(异常——DHL 那条模拟了海关扣留)
五、Agent Prompt 如何引导工具调用

插件写好了,如果 Agent 不知道什么时候该调、怎么调,也是白搭。工具调用的质量,很大程度取决于 Agent 的 System Prompt。

关键 Prompt 规则

Decision Procedure
  1. Extract identifiers: Scan for order numbers (EF prefix, Amazon 3-7-7, eBay 2-5-5)
    or tracking numbers (SF/1Z/TBA prefix).
  2. Handle missing information:
    • No order number → Ask for it. Do NOT call the tool.
    • No tracking number → Ask for it. Do NOT call the tool.
  3. Call the correct tool with the extracted identifier.
  4. Handle tool results:
    • Data found → Present clearly with bullet points.
    • NOT_FOUND → “I couldn’t find information for [number].”
  5. Auto-cascade: If order query returns a tracking number,
    you may auto-call logistics_query.

**5 条规则的精妙之处**:

| 规则 | 解决的问题 |
|------|-----------|
| 规则 1:Extract identifiers | Agent 先提取参数再调工具,而不是把整句话传给工具 |
| 规则 2:Missing info → Ask | 防止 Agent 拿空参数调工具(返回无意义的错误) |
| 规则 3:Call the correct tool | 两个工具,明确告诉 Agent 什么时候用哪个 |
| 规则 4:Handle tool results | Agent 不直接转发 JSON,而是用自然语言转述 |
| 规则 5:Auto-cascade | 一次查询拿到完整信息(订单+物流),用户不用问两次 |

**实际交互效果**

用户: 查一下订单 EFUS-121453

Agent: 正在为您查询订单 EFUS-121453…

  • 订单号: EFUS-121453
  • 状态: 已发货
  • 商品: DELTA 3 Plus x1
  • 快递公司: SF Express
  • 快递单号: SF1234567890
  • 预计送达: 2026-04-25

物流最新动态:

  • 2026-04-17 旧金山 – 派送中
  • 2026-04-16 洛杉矶 – 到达分拣中心

注意:Agent 自动级联调用了 `logistics_query`,因为订单查询返回了 `trackingNumber`。用户只说了一句话,拿到了订单+物流的完整信息。
六、生产环境切换清单

从 Mock 切到真实 API,只需 3 个环境变量:

在 RAGFlow 容器中设置

docker exec <容器名> bash -c “echo ‘ECOFLOW_MOCK_MODE=false’ >> /etc/environment”
docker exec <容器名> bash -c “echo ‘ECOFLOW_API_BASE_URL=https://api.ecoflow.com’ >> /etc/environment”
docker exec <容器名> bash -c “echo ‘ECOFLOW_API_KEY=<真实Key>’ >> /etc/environment”
docker restart <容器名>


**切换后验证**:

1. 用真实订单号测试订单查询
2. 用真实快递单号测试物流查询
3. 故意传错误单号,确认错误处理正常
4. 检查日志中的 `mock: false` 标记
七、插件部署流程
1. 复制插件到容器

docker cp plugins/ecoflow_order.py <容器名>:/ragflow/agent/tools/
docker cp plugins/ecoflow_logistics.py <容器名>:/ragflow/agent/tools/

2. 重启容器使插件生效

docker restart <容器名>

3. 在工作流中绑定插件
方式一:JSON 导入后,在 Agent 节点手动添加工具
方式二:Python 脚本部署时自动绑定
八、经验总结
经验 说明
Mock 先行 开发阶段不依赖真实 API,测试阶段不担心误操作
环境变量驱动 一行配置切换 Mock/真实,零代码改动
单号格式自动识别 让用户直接输入单号,不需要选择平台
Mock 覆盖全场景 正常 + 异常 + 边界,每个状态都有对应数据
Prompt 引导工具调用 明确的 5 步决策流程,减少误调用和空调用
级联调用 Agent 自动串联两个工具,一次输入拿到完整信息

一句话总结:插件开发的难点不在代码本身,而在——Mock 数据要足够真实、格式识别要足够鲁棒、Prompt 要足够精确地引导 Agent 正确使用工具。

作者:AI技术实践团队本文方案已在 RAGFlow v0.23.1 上验证,欢迎交流。