给 RAGFlow Agent 写插件:从 Mock 到生产的全链路
AI 客服最核心的价值不是”能聊天”,而是”能办事”——查订单、追物流,这需要给 Agent 接入真实业务 API。但开发阶段没有真实接口怎么办?Mock 先行,生产无缝切换。
本文以 RAGFlow 平台为例,分享从插件开发、Mock 数据设计、单号格式识别,到生产环境切换的完整工程实践。
RAGFlow 的 Agent 节点可以绑定”工具”(Tool),工具就是一个 Python 类,放在 agent/tools/ 目录下,运行时被 Agent 调用。
插件的三个核心要素
from agent.tools.base import ToolParamBase, ToolBase, ToolMeta
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,
},
},
}
class EcoflowOrderQuery(ToolBase, ABC):
component_name = “EcoflowOrderQuery”
@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_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),
“Amazon”: re.compile(r”\d{3}-\d{7}-\d{7}”),
“eBay”: re.compile(r”\d{2}-\d{5}-\d{5}”),
}
自动识别逻辑:
```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”,
},
“113-1234567-1234567”: {
“orderNumber”: “113-1234567-1234567”,
“status”: “SHIPPED”,
“trackingNumber”: “TBA123456789000”,
“carrier”: “Amazon Logistics”,
},
“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)),
(“UPS”, re.compile(r”^1Z[0-9A-Z]{16}${paragraph}quot;, re.IGNORECASE)),
(“Amazon Logistics”, re.compile(r”^TBA\d{10,15}${paragraph}quot;, re.IGNORECASE)),
(“USPS”, re.compile(r”^9[234]\d{18,22}${paragraph}quot;)),
(“DHL”, re.compile(r”^(JD|JJD)\d{10,18}${paragraph}quot;, re.IGNORECASE)),
(“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 不知道什么时候该调、怎么调,也是白搭。工具调用的质量,很大程度取决于 Agent 的 System Prompt。
关键 Prompt 规则
- Extract identifiers: Scan for order numbers (EF prefix, Amazon 3-7-7, eBay 2-5-5)
or tracking numbers (SF/1Z/TBA prefix). - 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.
- Call the correct tool with the extracted identifier.
- Handle tool results:
- Data found → Present clearly with bullet points.
- NOT_FOUND → “I couldn’t find information for [number].”
- 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 个环境变量:
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` 标记
docker cp plugins/ecoflow_order.py <容器名>:/ragflow/agent/tools/
docker cp plugins/ecoflow_logistics.py <容器名>:/ragflow/agent/tools/
docker restart <容器名>
| 经验 | 说明 |
|---|---|
| Mock 先行 | 开发阶段不依赖真实 API,测试阶段不担心误操作 |
| 环境变量驱动 | 一行配置切换 Mock/真实,零代码改动 |
| 单号格式自动识别 | 让用户直接输入单号,不需要选择平台 |
| Mock 覆盖全场景 | 正常 + 异常 + 边界,每个状态都有对应数据 |
| Prompt 引导工具调用 | 明确的 5 步决策流程,减少误调用和空调用 |
| 级联调用 | Agent 自动串联两个工具,一次输入拿到完整信息 |
一句话总结:插件开发的难点不在代码本身,而在——Mock 数据要足够真实、格式识别要足够鲁棒、Prompt 要足够精确地引导 Agent 正确使用工具。
作者:AI技术实践团队本文方案已在 RAGFlow v0.23.1 上验证,欢迎交流。
夜雨聆风