browse.nuvatech.cn Wiki 导出
- - 导出时间:
2026-05-08T02:40:37.206089759Z - - Wiki ID:
2026-05-08-104037 - - 来源:
https://browse.nuvatech.cn
目录
- - 项目概览
- - 环境搭建与启动
- - 环境变量配置
- - 配置文件结构
- - 系统架构总览
- - 单主 Agent 架构
- - 技能加载与管理
- - 技能工具门控机制
- - Dialog State 设计
- - SOP 阶段推进
- - 用户确认机制
- - 工具注册与工厂
- - 工具描述管理
- - 工具调用链路追踪
- - 消息入站处理流程
- - 微信客服与小程序集成
- - 会话分发与回复状态
- - Mem0 记忆管理
- - 用户洞察分析
- - 穿搭工作流服务
- - 图片生成与处理
- - 单品初始化处理
- - Dashboard 管理面板
- - Admin API 接口
- - 链路追踪与 Trace
- - 系统监控与健康检查
- - 单元测试与集成测试
- - 自动化测试与修复
- - 部署与发布
- - 新增业务工具
- - 新增技能模块
- - 提示词管理与国际化
<a id="1-xiang-mu-gai-lan"></a>
---
项目概览
- - Section: 快速入门
- - Source File:
1-xiang-mu-gai-lan.md
outfit_agent 是一个基于 Agno 框架构建的智能穿搭顾问 REST API 服务。系统采用单主 Agent + 按需加载 Skill + SOP 状态推进的架构模式,为微信客服和微信小程序渠道提供专业的穿搭建议、单品管理和造型生成服务。
系统定位
outfit_agent 的核心定位是穿搭领域的智能顾问,而非通用聊天机器人。系统通过技能(Skill)机制实现专业能力的模块化加载,在穿搭构想、用户确认、搜索单品、穿搭输出等关键节点与用户进行显式交互,确保每一步决策都经过用户确认。
项目依赖同目录下的 outfit_biz 包,该包封装了穿搭业务的核心逻辑,包括数据库模型、业务工具函数和配置管理。outfit_agent 在其基础上构建 Agent 运行时、消息处理和渠道集成。
Sources: README.md, AGENTS.md, docs/agent_refact.md
技术架构
graph TB
subgraph "外部渠道"
WK[微信客服]
WM[微信小程序]
end
subgraph "outfit_agent 服务"
direction TB
FA[FastAPI 应用]
AO[AgentOS]
MA[主 Agent]
subgraph "技能系统"
SK1[style-advisor-skill]
SK2[outfit-output-skill]
SK3[buyer-wardrobe-skill]
SK4[nuva-soul]
end
subgraph "工具系统"
T1[穿搭工具]
T2[用户工具]
T3[图片工具]
T4[消息工具]
end
subgraph "服务层"
WS[工作流服务]
MS[消息服务]
IS[洞察服务]
end
end
subgraph "数据层"
PG[(PostgreSQL)]
RD[(Redis)]
M0[(Mem0)]
end
subgraph "外部服务"
LLM[LLM 模型]
OSS[对象存储]
WX[微信 API]
end
WK --> FA
WM --> FA
FA --> AO
AO --> MA
MA --> SK1 & SK2 & SK3 & SK4
SK1 & SK2 & SK3 & SK4 --> T1 & T2 & T3 & T4
MA --> WS & MS & IS
WS & MS & IS --> PG & RD & M0
MA --> LLM
T3 --> OSS
MS --> WX
系统采用分层架构设计:
入口层:FastAPI 应用通过路由模块接收来自不同渠道的请求,支持普通 JSON 响应和 SSE 流式响应。
Agent 运行时:基于 Agno 框架的 Agent 单例,负责意图识别、技能加载和工具调用。AgentOS 提供会话持久化、链路追踪和调度能力。
技能系统:通过 TOML 配置文件定义技能,每个技能包含工具列表、指令提示和激活条件。技能按需加载,与 SOP 阶段绑定。
工具系统:业务工具函数通过工厂模式注册,支持渠道门控和技能绑定。工具描述通过 descriptions.toml 管理,支持灰度覆盖。
服务层:工作流服务管理穿搭生成的长流程,消息服务处理多渠道消息分发,洞察服务负责用户画像的离线分析。
数据层:PostgreSQL 存储会话、用户和业务数据,Redis 提供缓存和队列能力,Mem0 管理长期语义记忆。
Sources: app/main.py, app/agentos.py, app/agent.py
核心模块
Agent 运行时
Agent 运行时是系统的核心,采用单例模式确保实例复用。主 Agent 负责意图识别和任务分发,通过技能机制动态加载专业能力。
# Agent 单例初始化
_agent: Agent | None = None
def init_agent() -> Agent:
"""初始化并返回 Agent 单例"""
global _agent
if _agent is not None:
return _agent
# ... 构建 Agent 实例
return _agent
Agent 配置通过 data/team/main.toml 定义,包括可用工具、技能列表和渠道专属工具。模型配置通过 config.yml 的 model_map 节管理,支持多模型灰度。
Sources: app/agent.py, data/team/main.toml
技能系统
技能系统实现专业能力的模块化管理。每个技能通过 skill.toml 定义元数据、工具列表和激活条件。
# 技能定义示例
id = "style-advisor-skill"
name = "Style Advisor"
description = "风格顾问技能"
activation = "on_demand"
[metadata]
category = "styling"
owner = "main-team"
[instructions]
zh = "skills/style_advisor.zh.txt"
en = "skills/style_advisor.en.txt"
技能支持以下特性:
- - 按需激活:根据用户意图和 SOP 阶段动态加载
- - 渠道门控:某些工具仅在特定渠道可用
- - 灰度控制:通过灰度配置服务控制技能对用户的可见性
- - 指令注入:技能指令通过 PromptLoader 从文件加载
Sources: app/skills/loader.py, data/skills/nuva-soul/skill.toml
工具系统
工具系统采用工厂模式,支持工具注册、描述管理和运行时门控。
# 工具注册示例
def materialize_tools_for_user(
tools: Sequence[Function | Callable[..., Any]],
*,
user_id: int | str | None = None,
) -> list[Function | Callable[..., Any]]:
"""为用户物化工具列表,应用描述和门控规则"""
# ...
工具通过 app/tools/ 目录下的模块定义,每个工具函数必须包含完整的 docstring,Agno 会据此生成函数 schema 供 LLM 调用。
工具描述通过 data/tools/descriptions.toml 集中管理,支持按用户灰度覆盖。
Sources: app/tools/factory.py, data/tools/descriptions.toml
消息处理
消息处理模块负责多渠道消息的接收、分发和回复。支持文本、图片和富媒体消息。
# 消息分发示例
async def dispatch_text(
text: str,
*,
tool_ctx: ToolContext,
channel: str,
# ...
) -> dict[str, Any]:
"""分发文本消息到 Agent 处理"""
# ...
消息处理流程:
- 1. 入站处理:验证、审计、快捷回复检测
- 2. 会话分发:根据会话状态决定处理方式
- 3. Agent 调用:将消息发送给 Agent 处理
- 4. 回复格式化:根据渠道特性格式化回复
- 5. 消息发送:通过渠道 API 发送回复
Sources: app/messaging/__init__.py, app/messaging/session_dispatcher.py
记忆与洞察
系统集成 Mem0 进行长期语义记忆管理,支持实时分析和离线洞察两种模式。
# 记忆操作示例
async def search_user_memory(
query: str,
user_id: str,
limit: int = 10,
) -> list[dict[str, Any]]:
"""搜索用户相关记忆"""
# ...
洞察服务通过定时任务分析用户行为,生成用户画像和风格偏好,用于个性化推荐。
Sources: app/memory/__init__.py, app/insights/__init__.py
数据流
系统处理用户请求的典型流程如下:
sequenceDiagram
participant U as 用户
participant C as 渠道
participant F as FastAPI
participant A as Agent
participant S as 技能
participant T as 工具
participant D as 数据层
U->>C: 发送消息
C->>F: HTTP 请求
F->>F: 入站处理(审计、快捷回复)
F->>A: 分发到 Agent
A->>A: 意图识别
A->>S: 加载相关技能
S->>T: 调用工具
T->>D: 读写数据
D-->>T: 返回结果
T-->>S: 工具结果
S-->>A: 技能结果
A->>A: 生成回复
A-->>F: Agent 响应
F->>C: 格式化回复
C->>U: 发送回复
对于穿搭生成等长流程,系统采用异步工作流模式:
- 1. 任务创建:创建穿搭生成任务,返回任务 ID
- 2. 后台执行:工作流服务异步执行多步骤流程
- 3. 状态更新:通过 WebSocket 或轮询向客户端推送进度
- 4. 结果交付:完成后通过渠道 API 发送结果
Sources: app/services/outfit_workflow_service.py, app/workflows/outfit_generation.py
配置管理
系统采用分层配置管理:
环境变量:通过 .env 文件管理敏感配置,如 API 密钥、数据库连接等。
配置文件:config.yml 管理业务配置,支持 outfit_biz/config.yml 基础配置和 outfit_agent/config.yml 覆盖配置的 deep-merge。
运行时配置:通过 AgentSettings 类提供类型安全的配置访问,支持从 YAML 和环境变量读取。
# 配置读取示例
settings = get_settings()
model_cfg = settings.get_model("default")
db_url = settings.database_url
配置支持灰度控制,通过 outfit_biz.gray 模块实现按用户的配置覆盖。
Sources: app/config.py, config.yml, .env.example
部署架构
系统采用 PM2 + uvicorn 的部署方式:
// ecosystem.config.js
module.exports = {
apps: [{
name: 'outfit-agent',
script: 'uvicorn',
args: 'app.main:app --host 0.0.0.0 --port 8000',
// ...
}]
}
部署依赖:
- - PostgreSQL:存储会话、用户、业务数据和 AgentOS 追踪数据
- - Redis:提供缓存、队列和分布式锁能力
- - 对象存储:存储用户上传图片和生成图片
- - LLM 服务:提供大语言模型能力,支持 OpenAI 兼容接口
Sources: ecosystem.config.js, app/main.py
测试体系
项目包含完整的测试体系,覆盖单元测试、集成测试和自动化测试:
- - 单元测试:覆盖工具函数、服务逻辑和配置管理
- - 集成测试:测试 API 端点和渠道集成
- - 自动化测试:基于场景的回归测试,支持自动修复
# 运行测试
pytest
# 运行特定测试
pytest tests/test_agentos.py
测试文件位于 tests/ 目录,共 103 个测试文件,覆盖系统各个模块。
Sources: tests/, pyproject.toml
开发规范
项目遵循严格的开发规范:
代码风格:
- - 使用 Ruff 进行代码格式化和 lint 检查
- - 行长度限制为 100 字符
- - 目标 Python 版本为 3.11
提示词管理:
- - 所有提示词禁止硬编码,必须通过 PromptLoader 从文件加载
- - 用户可见文本禁止硬编码,必须通过
get_text/get_file/get_prompt加载 - - 提示词文件位于
data/prompts/和data/i18n/
Agent 实例管理:
- - Agent 实例必须单例复用,禁止在循环中反复创建
- - 工具函数必须包含完整 docstring,Agno 据此生成函数 schema
配置管理:
- - 敏感配置通过环境变量管理
- - 业务配置通过 YAML 文件管理
- - 配置变更需要重启服务生效
Sources: AGENTS.md, pyproject.toml
下一步阅读
根据您的开发需求,建议按以下顺序阅读文档:
快速入门:
架构理解:
- - 系统架构总览:深入了解系统分层架构
- - 单主 Agent 架构:理解 Agent 运行时设计
- - 技能加载与管理:掌握技能系统的工作原理
核心功能:
- - Dialog State 设计:理解对话状态管理
- - 工具注册与工厂:了解工具系统的实现
- - 穿搭工作流服务:掌握穿搭生成流程
扩展开发:
<a id="2-huan-jing-da-jian-yu-qi-dong"></a>
---
环境搭建与启动
- - Section: 快速入门
- - Source File:
2-huan-jing-da-jian-yu-qi-dong.md
本页面指导开发者从零开始搭建 outfit_agent 的本地开发/部署环境,涵盖依赖安装、配置文件设置、数据库初始化及服务启动的完整流程。
一、前置条件
在开始之前,请确保系统已安装以下基础组件:
| 组件 | 最低版本 | 用途 | 安装方式 | |
|---|---|---|---|---|
| Python | 3.11+ | 核心运行时 | python.org | |
| uv | 最新版 | Python 包管理与运行 | `curl -LsSf https://astral.sh/uv/install.sh \ | sh` |
| PostgreSQL | 14+(推荐 16) | 主数据库(含向量检索扩展) | postgresql.org | |
| Redis | 6.0+ | 缓存/任务队列 | redis.io | |
| Node.js | 18+(可选) | Dashboard 前端构建 | nodejs.org | |
| Git | 最新版 | 代码管理 | git-scm.com |
关键数据库扩展:PostgreSQL 需安装 pgvector、pgvectorscale、pg_search 及 zhparser 扩展,用于向量检索与中文全文搜索。
Sources: pyproject.toml | README.md
二、项目结构概览
本项目采用 双仓库协作 架构:outfit_agent 依赖同级目录下的 outfit_biz 公共业务库。
graph TD
A[outfit_agent] -->|依赖| B[outfit_biz]
A -->|editable install| B
C[config.yml] -->|deep-merge| D[AgentSettings]
E[.env] -->|环境变量| D
B -->|提供| F[数据库模型 / 服务 / 工具]
A -->|包含| G[Agent / Skills / Routers]
G -->|调用| F
目录结构简览:
workspace/
├── outfit_agent/ # 本项目:Agent 应用层
│ ├── app/ # 核心代码(FastAPI + Agno Agent)
│ │ ├── main.py # 应用入口,lifespan 管理
│ │ ├── agent.py # Agent 单例初始化
│ │ ├── config.py # 配置加载(继承 outfit_biz.Settings)
│ │ ├── routers/ # HTTP 路由(chat/wechat/miniapp/admin)
│ │ ├── services/ # 业务服务
│ │ ├── tools/ # Agent 工具函数
│ │ ├── skills/ # 技能加载器
│ │ └── memory/ # Mem0 记忆管理
│ ├── data/ # 提示词/技能/团队配置/i18n
│ ├── dashboard/ # 前端管理面板(React + Vite)
│ ├── tests/ # 测试用例
│ ├── config.yml # Agent 专有配置(覆盖 outfit_biz 默认值)
│ ├── .env.example # 环境变量模板
│ ├── pyproject.toml # Python 项目元数据
│ └── ecosystem.config.js # PM2 生产部署配置
│
└── outfit_biz/ # 共享业务库:数据模型/仓库/工具
├── src/outfit_biz/ # 核心包
├── config.yml # 基础配置(模型映射/数据目录等)
└── docs/init.sql # 数据库初始化脚本
Sources: README.md | config.py
三、克隆代码
两个仓库必须放在同一父目录下(pyproject.toml 中 outfit-biz 使用了相对路径引用):
# 进入工作目录
mkdir -p ~/outfit && cd ~/outfit
# 克隆两个仓库
git clone <outfit_agent_repo_url> outfit_agent
git clone <outfit_biz_repo_url> outfit_biz
# 验证目录结构
ls -la
# 应同时看到 outfit_agent/ 和 outfit_biz/
重要:outfit_agent/pyproject.toml中声明了[tool.uv.sources] outfit-biz = { path = "../outfit_biz", editable = true },因此两个仓库的相对路径必须保持../outfit_biz。
Sources: pyproject.toml
四、安装 Python 依赖
本项目使用 uv 作为包管理工具,它会自动创建虚拟环境并安装所有依赖:
cd ~/outfit/outfit_agent
# 同步所有依赖(含开发依赖)
uv sync --dev
uv sync 会自动:
- 1. 读取
pyproject.toml和uv.lock中的依赖声明 - 2. 创建
.venv虚拟环境 - 3. 安装
outfit_agent自身及outfit_biz(editable 模式) - 4. 安装开发工具(pytest、ruff 等)
验证安装:
# 确认 Python 版本
uv run python --version
# Python 3.12.x
# 确认关键依赖
uv run python -c "import agno; print(agno.__version__)"
uv run python -c "import fastapi; print(fastapi.__version__)"
Sources: pyproject.toml | ecosystem.config.js
五、数据库初始化
5.1 创建数据库
# 创建数据库
psql -U postgres -c "CREATE DATABASE outfit_biz;"
5.2 安装 PostgreSQL 扩展
数据库需要以下扩展来支持向量检索和中文全文搜索:
-- 连接到数据库
\c outfit_biz
-- 安装扩展
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS vector; -- pgvector
CREATE EXTENSION IF NOT EXISTS vectorscale; -- pgvectorscale
CREATE EXTENSION IF NOT EXISTS pg_search; -- BM25 全文检索
CREATE EXTENSION IF NOT EXISTS zhparser; -- 中文分词
注意:pg_search来自 ParadeDB 项目,需单独安装。vectorscale是 Timescale 的高性能向量索引扩展。
5.3 初始化表结构
执行项目提供的初始化脚本:
psql -U postgres -d outfit_biz -f ../outfit_biz/docs/init.sql
该脚本会创建以下核心表:
- -
users/user_profiles/user_oauth— 用户体系 - -
conversations/messages— 对话与消息 - -
items/images— 衣橱单品与图片(分区表) - -
outfits— 穿搭方案 - -
gray_config_rules— 灰度配置 - -
user_insights— 用户洞察 - -
message_access_*— 消息准入控制
注意:首次运行应用时,app/main.py的 lifespan 会自动执行增量迁移(ALTER TABLE ... ADD COLUMN IF NOT EXISTS),因此即使init.sql版本略有滞后,应用也能自动补齐缺失字段。
六、配置环境变量
6.1 创建 .env 文件
cp .env.example .env
6.2 必选配置项
以下配置项为服务正常运行的最低要求:
| 配置项 | 说明 | 示例值 |
|---|---|---|
OPENAI_API_KEY | LLM 服务 API Key(或兼容网关 Key) | sk-xxxx |
DATABASE_URL | PostgreSQL 连接 URL | postgresql+asyncpg://postgres:postgres@localhost:5432/outfit_biz |
REDIS_URL | Redis 连接地址 | redis://localhost:6379/0 |
6.3 推荐配置项
| 配置项 | 说明 | 默认值 |
|---|---|---|
OPENAI_BASE_URL | LLM 网关地址(如使用代理) | 空(使用官方地址) |
DASHSCOPE_API_KEY | 阿里云 DashScope API Key(视觉/Embedding 模型) | 空 |
MEM0_ENABLED | 是否启用 Mem0 长期记忆 | true |
ADMIN_SECRET_KEY | Admin Dashboard 访问密钥 | 空(禁用 Admin 接口) |
LOG_LEVEL | 日志级别 | info |
6.4 可选配置项
根据开发需求,按需配置以下模块:
企业微信接入:
WECHAT_TOKEN=your_token
WECHAT_ENCODING_AES_KEY=your_key
WECHAT_CORP_ID=your_corp_id
WECHAT_SECRET=your_secret
阿里云内容安全:
ALIYUN_AK_ID=your_access_key_id
ALIYUN_AK_SECRET=your_access_key_secret
和风天气:
QWEATHER_API_HOST=your_host.qweatherapi.com
QWEATHER_KEY_ID=your_key_id
QWEATHER_PROJECT_ID=your_project_id
QWEATHER_PRIVATE_KEY_PATH=data/ed25519-private.pem
AI 生图质量:
IMAGE_GEN_QUALITY=2K # 可选: 1K, 2K, 4K
完整配置参考:所有可用的环境变量详见 .env.example。
Sources: .env.example | config.py
七、配置文件说明
除了环境变量,项目还使用 YAML 配置文件进行更细粒度的行为控制。配置文件采用深度合并策略:outfit_biz/config.yml 提供基础配置,outfit_agent/config.yml 在其上叠加覆盖。
7.1 config.yml 结构
graph LR
A[outfit_biz/config.yml] -->|基础| C[合并后配置]
B[outfit_agent/config.yml] -->|覆盖| C
D[环境变量 .env] -->|最高优先级| C
C --> E[AgentSettings]
优先级(从高到低):
- 1. 环境变量 /
.env - 2.
config.yml合并值 - 3. 代码中的默认值
7.2 关键配置节
| 配置节 | 说明 | 配置文件 |
|---|---|---|
model_map | LLM 模型映射(default/flash/vision/embedding 等角色) | outfit_biz/config.yml |
agent.team | 主 Team 构造参数(历史记录数/重试策略等) | outfit_agent/config.yml |
agent.member | 子 Agent 构造参数 | outfit_agent/config.yml |
agent.leader_model | 主 Team 使用的模型角色 | outfit_agent/config.yml |
7.3 模型映射示例
config.yml 中的 model_map 定义了不同角色使用的 LLM 模型:
model_map:
default: # 默认模型,其他角色继承其未覆盖的字段
model: qwen3.6-plus
provider: dashscope
api_base: https://dashscope.aliyuncs.com/compatible-mode/v1
flash: # 快速模型
model: qwen3.5-flash
vision: # 视觉理解模型
model: qwen-vl-plus
provider: dashscope
text_embedding: # 文本向量模型
model: text-embedding-v4
dimension: 1024
embedding: # 多模态向量模型
model: qwen3-vl-embedding
dimension: 512
提示:所有模型可通过OPENAI_API_KEY+OPENAI_BASE_URL统一走 OpenAI 兼容网关,无需为每个模型单独配置 Key。
Sources: config.yml | outfit_biz/config.yml
八、启动服务
8.1 开发模式启动
cd ~/outfit/outfit_agent
# 使用 uv 启动(自动加载 .env,开启热重载)
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
启动成功后会看到类似日志:
Initializing Agent singleton (sync, before AgentOS construction)...
Global agno async httpx client initialized with timeout=120s
Starting up: initializing Memory (Mem0) singleton...
ImgGenHandler started
ImageCaptionHandler started
ItemProcessingHandler started
AgentOS built: tracing=True, scheduler=True, mcp=True
AgentOS get_app() complete — all routes registered.
INFO: Uvicorn running on http://0.0.0.0:8000
8.2 生产模式启动(PM2)
项目提供了 ecosystem.config.js 用于 PM2 进程管理:
# 安装 PM2(如未安装)
npm install -g pm2
# 使用 PM2 启动
pm2 start ecosystem.config.js
# 查看日志
pm2 logs outfit-agent
# 查看状态
pm2 status
PM2 配置说明(ecosystem.config.js):
- - 启动命令:
uv run uvicorn app.main:app --host 127.0.0.1 --port 8897 - - 自动重启:
autorestart: true - - 内存限制:
max_memory_restart: "1G"
8.3 启动流程图
sequenceDiagram
participant M as main.py
participant A as Agent Init
participant L as Lifespan
participant S as Services
M->>M: load_dotenv(.env)
M->>M: _init_httpx_client()
M->>A: init_agent() + init_canvas_agent()
M->>M: init_hyperdx_monitoring()
M->>M: 注册路由 (chat/wechat/miniapp/admin)
M->>L: AgentOS lifespan 启动
Note over L: _mem0_lifespan
L->>L: DB schema ensure (ALTER TABLE)
L->>L: ensure_gray_config_schema()
L->>L: ensure_insight_schema()
L->>L: init_memory() (Mem0)
L->>L: _init_litellm_redis_cache()
L->>S: ImgGenHandler.start()
L->>S: ImageCaptionHandler.start()
L->>S: ItemProcessingHandler.start()
L-->>M: 服务就绪
Sources: main.py | ecosystem.config.js
九、验证服务
9.1 健康检查
curl http://localhost:8000/health
# 期望返回: {"status": "ok"}
9.2 API 文档
启动后访问 Swagger UI 查看完整 API 文档:
http://localhost:8000/docs
9.3 开发模式测试请求
配置 DEV_TOKEN 和 DEV_USER_ID 后,可直接调用 /api/chat 接口:
curl -X POST http://localhost:8000/api/chat \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your_dev_token" \
-d '{
"message": "你好,请帮我搭配一套春季穿搭",
"user_id": 1
}'
9.4 运行测试
# 运行全部测试
uv run pytest
# 运行特定测试文件
uv run pytest tests/test_agentos.py -v
# 带覆盖率
uv run pytest --cov=app --cov-report=term-missing
十、Dashboard 前端构建(可选)
Dashboard 是基于 React + Vite 的管理面板,用于调试 Agent 对话、查看 Trace 和配置管理。
cd ~/outfit/outfit_agent/dashboard
# 安装前端依赖
npm install
# 开发模式(Vite dev server)
npm run dev
# 生产构建(输出到 dist/)
npm run build
构建后,dist/ 目录会被 FastAPI 自动挂载到 /dashboard/ 路径下:
# app/main.py 中的自动挂载逻辑
_dashboard_dist = Path(__file__).resolve().parents[1] / "dashboard" / "dist"
if _dashboard_dist.exists():
base_app.mount("/dashboard", StaticFiles(directory=str(_dashboard_dist), html=True))
开发模式下:可直接访问 http://localhost:5173(Vite 默认端口)进行前端开发,API 请求通过 Vite proxy 转发到后端。
Sources: dashboard/package.json | main.py
十一、常见问题排查
Q1: 启动时报 ModuleNotFoundError: No module named 'outfit_biz'
原因:未执行 uv sync 或 outfit_biz 目录不在 ../outfit_biz 位置。
解决:
# 确认目录结构
ls ../outfit_biz/pyproject.toml
# 重新同步依赖
uv sync --dev
Q2: 启动时报 connection to server at "localhost" ... failed
原因:PostgreSQL 未启动或连接配置错误。
解决:
# 检查 PostgreSQL 是否运行
pg_isready
# 验证连接
psql -U postgres -d outfit_biz -c "SELECT 1;"
# 确认 .env 中 DATABASE_URL 配置正确
Q3: 启动时报 Redis 连接失败
原因:Redis 未启动或地址配置错误。
解决:
# 检查 Redis 是否运行
redis-cli ping
# 期望返回: PONG
# 确认 .env 中 REDIS_URL 配置正确
Q4: LLM 调用超时
原因:默认超时 120 秒,大型模型在负载下可能需要更长时间。
解决:调整 config.yml 中的超时配置:
llm_chat_timeout_seconds: 600 # 增加到 10 分钟
Q5: pgvector 或 pg_search 扩展安装失败
原因:这些扩展需要从源码编译或使用特定发行版。
解决:
# Ubuntu/Debian: 使用 PGDG 仓库
sudo apt install postgresql-16-pgvector
# 或从源码编译 pgvector
git clone https://github.com/pgvector/pgvector.git
cd pgvector
make && sudo make install
十二、下一步
环境搭建完成后,建议按以下顺序继续阅读:
- 1. 环境变量配置 — 详细了解每个配置项的用途和调优建议
- 2. 配置文件结构 — 深入理解 config.yml 的合并策略和完整字段
- 3. 系统架构总览 — 从全局视角理解系统各组件的协作关系
- 4. 单元测试与集成测试 — 学习如何编写和运行测试
- 5. 新增业务工具 — 扩展 Agent 能力的第一步
<a id="3-huan-jing-bian-liang-pei-zhi"></a>
---
环境变量配置
- - Section: 快速入门
- - Group: 配置与环境
- - Source File:
3-huan-jing-bian-liang-pei-zhi.md
本文档详细说明 outfit_agent 项目中所有环境变量的配置方式、用途和优先级规则。环境变量是系统配置的最高优先级来源,掌握这些变量的正确配置对于项目的正常运行至关重要。
配置加载机制
outfit_agent 的配置系统采用多层叠加架构,优先级从高到低依次为:
- 1. 环境变量(最高优先级):直接在操作系统或
.env文件中设置 - 2.
.env文件:项目根目录下的环境变量文件 - 3.
config.yml配置文件:outfit_agent 和 outfit_biz 两个项目的 YAML 配置叠加 - 4. 代码默认值:Python 代码中定义的字段默认值
配置加载流程如下:
flowchart TD
A[应用启动] --> B[加载 .env 文件]
B --> C[加载 outfit_biz/config.yml]
C --> D[加载 outfit_agent/config.yml]
D --> E[深度合并配置]
E --> F[环境变量覆盖]
F --> G[创建 AgentSettings 单例]
subgraph "配置源优先级"
H[环境变量] --> I[.env 文件]
I --> J[config.yml]
J --> K[代码默认值]
end
配置加载的核心逻辑在 app/config.py 和 outfit_biz/src/outfit_biz/config.py 中实现。
Sources: app/config.py, outfit_biz/src/outfit_biz/config.py
核心环境变量清单
以下是系统运行所需的核心环境变量,按功能模块分类:
LLM 模型配置
| 变量名 | 默认值 | 说明 | 必填 |
|---|---|---|---|
OPENAI_API_KEY | - | OpenAI 兼容 API 的密钥 | ✅ |
OPENAI_BASE_URL | - | OpenAI 兼容 API 的基础 URL(可选) | ❌ |
DASHSCOPE_API_KEY | - | DashScope API 密钥(旧版兼容) | ❌ |
DEEPSEEK_API_KEY | - | DeepSeek API 密钥 | ❌ |
DEEPSEEK_BASE_URL | - | DeepSeek API 基础 URL | ❌ |
LITELLM_LOCAL_MODEL_COST_MAP | True | 禁用 LiteLLM 从 GitHub 拉取模型价格表 | ❌ |
LITELLM_REDIS_CACHE_ENABLED | false | 启用 LiteLLM Redis LLM 响应缓存 | ❌ |
LITELLM_REDIS_TTL | 3600 | LiteLLM 缓存 TTL(秒) | ❌ |
LLM 配置推荐统一走 OpenAI 兼容网关,模型别名和默认参数在 config.yml 的 model_map 节配置。
Sources: .env.example, app/config.py
服务配置
| 变量名 | 默认值 | 说明 | 必填 |
|---|---|---|---|
HOST | 0.0.0.0 | 服务监听地址 | ❌ |
PORT | 8000 | 服务监听端口 | ❌ |
LOG_LEVEL | info | 日志级别(DEBUG/INFO/WARNING/ERROR/CRITICAL) | ❌ |
AGENT_NAME | outfit-agent | Agent 实例名称 | ❌ |
DEBUG | false | 调试模式开关 | ❌ |
Sources: .env.example, app/config.py
数据库与缓存配置
| 变量名 | 默认值 | 说明 | 必填 |
|---|---|---|---|
DATABASE_URL | postgresql+asyncpg://postgres:postgres@localhost:5432/outfit | PostgreSQL 数据库连接 URL | ✅ |
DATABASE_POOL_SIZE | 10 | 数据库连接池大小 | ❌ |
DATABASE_MAX_OVERFLOW | 20 | 连接池最大溢出数 | ❌ |
REDIS_URL | redis://localhost:6379/0 | Redis 连接 URL | ❌ |
MEM0_ENABLED | true | 启用 Mem0 记忆组件 | ❌ |
MEM0_REALTIME_ANALYSIS_ENABLED | false | 启用每条对话实时 mem0 提取/写入 | ❌ |
MEM0_COLLECTION_NAME | outfit_mem | Mem0 集合名称 | ❌ |
MEM0_SEARCH_LIMIT | 10 | Mem0 搜索结果数量限制 | ❌ |
MEM0_HISTORY_DB_PATH | data/mem0_history.db | mem0 历史记录 SQLite 路径 | ❌ |
数据库和 Redis 是系统的核心基础设施。Mem0 组件负责长期语义记忆,完整对话历史由 agno PostgresDb 持久化。
Sources: .env.example, app/config.py, app/memory/client.py
企业微信配置
| 变量名 | 默认值 | 说明 | 必填 |
|---|---|---|---|
WECHAT_TOKEN | - | 企业微信客服 Token | ❌ |
WECHAT_ENCODING_AES_KEY | - | 企业微信消息加密密钥 | ❌ |
WECHAT_CORP_ID | - | 企业微信企业 ID | ❌ |
WECHAT_SECRET | - | 企业微信应用 Secret | ❌ |
WECHAT_MINIPROGRAM_APPID | - | 微信小程序 AppID | ❌ |
WECHAT_MINIPROGRAM_TITLE | 点击打开小程序 | 小程序卡片标题 | ❌ |
WECHAT_MINIPROGRAM_PAGEPATH | pages/index/index | 小程序卡片跳转路径 | ❌ |
WECHAT_MINIPROGRAM_IMAGE_URL | - | 小程序卡片缩略图 URL | ❌ |
MESSAGE_ACCESS_CONTROL_ENABLED | true | 启用消息访问控制 | ❌ |
企业微信客服配置用于接收客服消息回调。小程序配置用于发送小程序卡片消息。
Sources: .env.example, app/config.py
阿里云内容安全配置
| 变量名 | 默认值 | 说明 | 必填 |
|---|---|---|---|
ALIYUN_AK_ID | - | 阿里云 AccessKey ID | ❌ |
ALIYUN_AK_SECRET | - | 阿里云 AccessKey Secret | ❌ |
ALIYUN_CONTENT_AUDIT_ENABLED | true | 启用内容安全审核 | ❌ |
ALIYUN_GREEN_ENDPOINT | green-cip.cn-shanghai.aliyuncs.com | Green SDK 端点 | ❌ |
ALIYUN_GREEN_REGION | cn-shanghai | Green SDK 区域 | ❌ |
ALIYUN_GREEN_CONNECT_TIMEOUT_MS | 3000 | 连接超时(毫秒) | ❌ |
ALIYUN_GREEN_READ_TIMEOUT_MS | 6000 | 读取超时(毫秒) | ❌ |
ALIYUN_TEXT_AUDIT_ENABLED | true | 启用文本审核 | ❌ |
ALIYUN_TEXT_AUDIT_SERVICE | comment_detection_pro | 文本审核服务 | ❌ |
ALIYUN_TEXT_AUDIT_BLOCKING_RISK_LEVELS | high | 文本审核阻断风险等级 | ❌ |
ALIYUN_IMAGE_AUDIT_ENABLED | true | 启用图片审核 | ❌ |
ALIYUN_IMAGE_AUDIT_SERVICE | baselineCheck | 图片审核服务 | ❌ |
ALIYUN_IMAGE_AUDIT_BLOCKING_RISK_LEVELS | high | 图片审核阻断风险等级 | ❌ |
阿里云内容安全用于对用户发送的文本和图片进行合规性审核。
Sources: .env.example, app/config.py
和风天气配置
| 变量名 | 默认值 | 说明 | 必填 |
|---|---|---|---|
QWEATHER_API_HOST | - | 和风天气 API Host(格式:abc123xyz.def.qweatherapi.com) | ❌ |
QWEATHER_KEY_ID | - | 凭据 ID(kid),在控制台-项目管理中查看 | ❌ |
QWEATHER_PROJECT_ID | - | 项目 ID(sub),在控制台-项目管理中查看 | ❌ |
QWEATHER_PRIVATE_KEY_PATH | data/ed25519-private.pem | Ed25519 私钥文件路径 | ❌ |
和风天气配置用于提供穿搭场景的天气查询能力。私钥文件需要从和风天气控制台生成。
Sources: .env.example, app/config.py, app/weather/client.py
图片生成配置
| 变量名 | 默认值 | 说明 | 必填 |
|---|---|---|---|
IMAGE_GEN_QUALITY | 2K | AI 生图质量(1K/2K/4K) | ❌ |
IMAGE_GEN_PROVIDER | gemini | 生图提供商 | ❌ |
IMAGE_GEN_PROVIDERS | - | 逗号分隔的提供商优先级列表 | ❌ |
GEMINI_API_KEY | - | Gemini API 密钥 | ❌ |
GEMINI_IMAGE_MODEL | gemini-2.0-flash-preview-image-generation | Gemini 生图模型 | ❌ |
OPENAI_IMAGE_API_KEY | - | OpenAI 生图 API 密钥 | ❌ |
OPENAI_IMAGE_MODEL | gpt-image-1 | OpenAI 生图模型 | ❌ |
ARK_API_KEY | - | 火山引擎 Ark API 密钥 | ❌ |
图片生成配置支持多个提供商,可以通过 IMAGE_GEN_PROVIDERS 设置优先级顺序。
Sources: .env.example, outfit_biz/src/outfit_biz/config.py
Admin Dashboard 配置
| 变量名 | 默认值 | 说明 | 必填 |
|---|---|---|---|
ADMIN_SECRET_KEY | - | Admin API 访问 Token(所有 /api/admin/* 接口需要) | ❌ |
DEV_TOKEN | - | 本地开发测试用 Token(仅开发环境) | ❌ |
DEV_USER_ID | 1 | 开发环境默认用户 ID | ❌ |
Admin Secret Key 是独立于用户 JWT 的管理接口认证凭证。生产环境必须设置一个随机长字符串,留空则 Admin 接口全部返回 403。
Sources: .env.example, app/config.py
监控与可观测性配置
| 变量名 | 默认值 | 说明 | 必填 |
|---|---|---|---|
HYPERDX_API_KEY | - | HyperDX 应用监控 API Key | ❌ |
OTEL_SERVICE_NAME | - | OpenTelemetry 服务名称 | ❌ |
HYPERDX_API_ENDPOINT | https://in-otel.hyperdx.io | HyperDX 端点 | ❌ |
OTEL_EXPORTER_OTLP_ENDPOINT | - | OpenTelemetry OTLP 端点 | ❌ |
OTEL_SERVICE_VERSION | - | 服务版本号 | ❌ |
HyperDX 监控通过 OpenTelemetry 协议发送日志数据。配置 HYPERDX_API_KEY 和 OTEL_SERVICE_NAME 后自动启用。
Sources: .env.example, app/observability.py
对象存储配置
| 变量名 | 默认值 | 说明 | 必填 |
|---|---|---|---|
OSS_ENDPOINT | - | 对象存储端点 | ❌ |
OSS_ENDPOINT_PUB | - | 公网访问端点 | ❌ |
OSS_REGION | - | 存储区域 | ❌ |
OSS_ACCESS_KEY_ID | - | AccessKey ID | ❌ |
OSS_ACCESS_KEY_SECRET | - | AccessKey Secret | ❌ |
OSS_BUCKET_NAME | - | 存储桶名称 | ❌ |
OSS_CDN_DOMAIN | - | CDN 域名 | ❌ |
OSS_SIGN_EXPIRE_SECONDS | 3600 | 签名过期时间(秒) | ❌ |
对象存储用于存储用户上传的图片和 AI 生成的穿搭图。
Sources: outfit_biz/src/outfit_biz/config.py
穿搭工作流配置
| 变量名 | 默认值 | 说明 | 必填 |
|---|---|---|---|
IMG_GEN_MAX_CONCURRENCY | 3 | 图片生成最大并发数 | ❌ |
IMG_GEN_QUEUE_POP_TIMEOUT_SECONDS | 1 | 队列弹出超时(秒) | ❌ |
IMG_GEN_DB_RECONCILE_INTERVAL_SECONDS | 180 | 数据库对账间隔(秒) | ❌ |
IMG_GEN_DB_BATCH_SIZE | 100 | 数据库对账批次大小 | ❌ |
IMG_GEN_QUEUE_QUEUED_TTL_SECONDS | 600 | 队列中任务 TTL(秒) | ❌ |
IMG_GEN_PROCESSING_LOCK_TTL_SECONDS | 1800 | 处理锁 TTL(秒) | ❌ |
LLM_CHAT_TIMEOUT_SECONDS | 600 | LLM 聊天超时(秒) | ❌ |
LLM_CHAT_MAX_RETRIES | 0 | LLM 聊天最大重试次数 | ❌ |
ITEM_INIT_MAX_CONCURRENCY | 2 | 单品初始化最大并发数 | ❌ |
IMAGE_CAPTION_MAX_CONCURRENCY | 2 | 图片描述最大并发数 | ❌ |
这些配置控制穿搭工作流的异步处理行为,包括图片生成、单品初始化和图片描述等后台任务。
Sources: app/config.py, app/main.py
用户洞察配置
| 变量名 | 默认值 | 说明 | 必填 |
|---|---|---|---|
INSIGHT_OUTFIT_DELTA_THRESHOLD | 20 | 穿搭洞察触发阈值 | ❌ |
INSIGHT_ITEM_DELTA_THRESHOLD | 20 | 单品洞察触发阈值 | ❌ |
INSIGHT_CHAT_INTERVAL_DAYS | 30 | 对话洞察间隔天数 | ❌ |
INSIGHT_MERGE_HISTORY_DAYS | 730 | 洞察合并历史天数 | ❌ |
INSIGHT_TONE_HISTORY_DAYS | 730 | 语气分析历史天数 | ❌ |
INSIGHT_CHAT_OVERLAP_MESSAGES | 5 | 对话重叠消息数 | ❌ |
INSIGHT_SCHEDULER_CRON | 0 0 * | 洞察调度 Cron 表达式 | ❌ |
INSIGHT_SCHEDULER_TIMEZONE | Asia/Shanghai | 调度时区 | ❌ |
INSIGHT_BATCH_SIZE | 200 | 批量处理大小 | ❌ |
INSIGHT_USER_CONCURRENCY | 5 | 用户处理并发数 | ❌ |
用户洞察是离线任务,每天定时分析用户的穿搭记录、衣橱单品和对话记录,生成用户画像并同步到 Mem0。
Sources: app/config.py, docs/insight.md
配置文件结构
环境变量与配置文件的关系如下:
graph TB
subgraph "环境变量优先级"
ENV[环境变量 / .env]
YAML_BIZ[outfit_biz/config.yml]
YAML_AGENT[outfit_agent/config.yml]
DEFAULT[代码默认值]
end
ENV --> MERGE[深度合并]
YAML_BIZ --> MERGE
YAML_AGENT --> MERGE
DEFAULT --> MERGE
MERGE --> SETTINGS[AgentSettings 单例]
subgraph "配置文件位置"
F1[.env]
F2[outfit_biz/config.yml]
F3[outfit_agent/config.yml]
end
F1 --> ENV
F2 --> YAML_BIZ
F3 --> YAML_AGENT
配置文件的叠加规则:
- 1.
outfit_biz/config.yml是基础配置 - 2.
outfit_agent/config.yml在基础上叠加(deep-merge) - 3. 环境变量具有最高优先级,会覆盖所有配置文件中的同名配置
Sources: app/config.py, outfit_biz/src/outfit_biz/config.py
配置最佳实践
1. 安全性原则
- - 敏感信息必须使用环境变量:API 密钥、数据库密码、Secret 等敏感信息不要写入配置文件
- -
.env文件必须加入.gitignore:防止密钥泄露到代码仓库 - - 生产环境使用随机长字符串:
ADMIN_SECRET_KEY、DEV_TOKEN等必须使用强密码
Sources: .gitignore, .env.example
2. 开发环境配置
# 复制示例文件
cp .env.example .env
# 编辑 .env 文件,填写必要的配置
# 至少需要配置:
# - OPENAI_API_KEY(LLM 调用)
# - DATABASE_URL(数据库连接)
# - REDIS_URL(Redis 缓存,可选)
开发环境可以使用 DEV_TOKEN 和 DEV_USER_ID 简化测试流程。
Sources: README.md, docs/deployment.md
3. 生产环境配置
生产环境配置建议:
- 1. 使用环境变量而非
.env文件:通过容器编排(Docker/K8s)注入环境变量 - 2. 启用内容安全审核:设置
ALIYUN_CONTENT_AUDIT_ENABLED=true - 3. 配置监控:设置
HYPERDX_API_KEY和OTEL_SERVICE_NAME - 4. 启用 Redis 缓存:设置
LITELLM_REDIS_CACHE_ENABLED=true降低 LLM 调用成本 - 5. 配置合理的并发参数:根据服务器资源调整
IMG_GEN_MAX_CONCURRENCY等参数
Sources: ecosystem.config.js, docs/deployment.md
4. 配置验证
系统启动时会自动验证配置的有效性:
- - 日志级别必须在
DEBUG/INFO/WARNING/ERROR/CRITICAL范围内 - - 数据库连接池大小必须大于 0
- - 超时时间必须为正数
- - 无效配置会使用默认值并输出警告日志
Sources: outfit_biz/src/outfit_biz/config.py
常见配置场景
场景 1:接入新的 LLM 提供商
如果需要接入新的 LLM 提供商(如 DeepSeek),需要配置:
DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
DEEPSEEK_BASE_URL=https://api.deepseek.com
然后在 config.yml 的 model_map 中配置模型别名。
Sources: outfit_biz/src/outfit_biz/config.py
场景 2:启用 Redis 缓存
启用 Redis 缓存可以显著降低 LLM 调用延迟和成本:
REDIS_URL=redis://localhost:6379/0
LITELLM_REDIS_CACHE_ENABLED=true
LITELLM_REDIS_TTL=3600
Sources: .env.example, app/main.py
场景 3:配置企业微信客服
配置企业微信客服需要以下变量:
WECHAT_TOKEN=your_token
WECHAT_ENCODING_AES_KEY=your_aes_key
WECHAT_CORP_ID=your_corp_id
WECHAT_SECRET=your_secret
WECHAT_MINIPROGRAM_APPID=your_appid
Sources: .env.example, app/config.py
场景 4:启用内容安全审核
启用阿里云内容安全审核:
ALIYUN_AK_ID=your_access_key_id
ALIYUN_AK_SECRET=your_access_key_secret
ALIYUN_CONTENT_AUDIT_ENABLED=true
ALIYUN_TEXT_AUDIT_ENABLED=true
ALIYUN_IMAGE_AUDIT_ENABLED=true
Sources: .env.example, app/config.py
配置变量速查表
| 功能模块 | 核心变量 | 可选变量 |
|---|---|---|
| LLM 模型 | OPENAI_API_KEY | OPENAI_BASE_URL, LITELLM_REDIS_CACHE_ENABLED |
| 数据库 | DATABASE_URL | DATABASE_POOL_SIZE, DATABASE_MAX_OVERFLOW |
| Redis | REDIS_URL | LITELLM_REDIS_TTL |
| 企业微信 | WECHAT_TOKEN, WECHAT_CORP_ID, WECHAT_SECRET | WECHAT_MINIPROGRAM_APPID |
| 内容安全 | ALIYUN_AK_ID, ALIYUN_AK_SECRET | ALIYUN_CONTENT_AUDIT_ENABLED |
| 天气 | QWEATHER_API_HOST, QWEATHER_KEY_ID, QWEATHER_PROJECT_ID | QWEATHER_PRIVATE_KEY_PATH |
| 图片生成 | IMAGE_GEN_QUALITY | IMAGE_GEN_PROVIDER, GEMINI_API_KEY |
| Admin | ADMIN_SECRET_KEY | DEV_TOKEN, DEV_USER_ID |
| 监控 | HYPERDX_API_KEY, OTEL_SERVICE_NAME | HYPERDX_API_ENDPOINT |
下一步
配置完成后,建议继续阅读:
<a id="4-pei-zhi-wen-jian-jie-gou"></a>
---
配置文件结构
- - Section: 快速入门
- - Group: 配置与环境
- - Source File:
4-pei-zhi-wen-jian-jie-gou.md
outfit-agent 的配置体系由四层配置源叠加而成:环境变量、.env 文件、config.yml YAML 配置、以及 TOML/JSON/文本等结构化数据文件。本页将系统梳理项目中所有配置文件的组织方式、加载优先级和各节用途。
配置层次总览
配置加载遵循明确的优先级链——高优先级源覆盖低优先级源的同名键值,且所有配置最终汇聚为 AgentSettings 单例对象。
graph TD
A["① 环境变量 / 系统环境"] -->|最高优先级| M["AgentSettings 单例"]
B["② .env 文件"] -->|次高| M
C["③ config.yml(biz层 + agent层 deep-merge)"] -->|基础| M
D["④ TOML / JSON / 文本数据文件"] -->|运行时按需加载| R["运行时组件"]
M --> R
subgraph "config.yml 合并过程"
C1["outfit_biz/config.yml"] -->|_deep_merge| C2["outfit_agent/config.yml"]
C2 --> C
end
Sources: app/config.py
环境变量与 .env 文件
.env.example 列出了所有可配置的环境变量,复制为 .env 后填入实际值即可。.env 已被 .gitignore 忽略,不会提交到版本库。
| 类别 | 典型变量 | 说明 |
|---|---|---|
| LLM 配置 | OPENAI_API_KEY, OPENAI_BASE_URL | 统一走 OpenAI 兼容网关 |
| 服务配置 | HOST, PORT, LOG_LEVEL | FastAPI 服务绑定参数 |
| 企业微信客服 | WECHAT_TOKEN, WECHAT_SECRET, WECHAT_CORP_ID 等 | 微信消息回调凭据 |
| 阿里云内容安全 | ALIYUN_AK_ID, ALIYUN_AK_SECRET 等 | 文本/图片内容审核凭据 |
| 和风天气 | QWEATHER_API_HOST, QWEATHER_KEY_ID, QWEATHER_PRIVATE_KEY_PATH | 天气查询 API 凭据 |
| Redis / 缓存 | LITELLM_REDIS_CACHE_ENABLED, LITELLM_REDIS_TTL | LiteLLM 响应缓存开关 |
| Mem0 记忆 | MEM0_ENABLED, MEM0_REALTIME_ANALYSIS_ENABLED, MEM0_COLLECTION_NAME | 长期语义记忆组件 |
| Admin Dashboard | ADMIN_SECRET_KEY | 管理后台 Bearer Token |
| 监控 | HYPERDX_API_KEY, OTEL_SERVICE_NAME | OpenTelemetry 应用监控 |
环境变量的值在 app.config.AgentSettings 中通过 Pydantic BaseSettings 机制自动注入,覆盖 .env 和 config.yml 中的同名字段。
Sources: .env.example, app/config.py
主配置文件 config.yml
config.yml 是 YAML 格式的主配置文件,采用分层叠加策略:outfit_biz/config.yml 作为基础层,outfit_agent/config.yml 作为覆盖层,两者通过 _deep_merge() 递归合并——agent 层的同名键覆盖 biz 层,嵌套字典则递归合并而非整体替换。
config.yml 中仅存放可序列化的标量值(bool / int / float / str),需要运行时对象(模型实例、数据库连接、工具列表、钩子函数等)的参数始终在 Python 代码中设置。
Sources: config.yml, app/config.py
顶层配置节
config.yml 的顶层键直接映射为 AgentSettings 的字段属性,按业务域分组:
| 配置节 | 典型键 | 说明 |
|---|---|---|
| Redis 缓存 | litellm_redis_cache_enabled, live_run_redis_ttl_seconds | LiteLLM 响应缓存、Live Run 心跳 TTL |
| Mem0 实时分析 | mem0_realtime_analysis_enabled | 控制每条对话后是否实时提取写入 mem0 |
| 阿里云内容安全 | aliyun_content_audit_enabled_channels, aliyun_*_blocking_confidence_threshold | 按渠道开启审核、置信度阈值 |
| LLM 超时/重试 | llm_chat_timeout_seconds, llm_chat_max_retries | 全局 LLM 调用策略 |
| 异步队列参数 | item_init_, image_caption_, img_gen_* | 衣橱补全、图片描述、图片生成的并发控制和队列 TTL |
| Embedding | embedding_batch_size_limit | Embedding 批处理上限 |
Sources: config.yml, app/config.py
agent 配置节
agent 节是 config.yml 的核心嵌套结构,控制 Agent 和 Team 的行为参数。该节下分为三个子节,分别透传给不同的构造函数:
graph TD
YAML["config.yml agent 节"] --> L["agent.leader_model"]
YAML --> T["agent.team"]
YAML --> M["agent.member"]
L -->|模型 alias| Agent["app/agent.py get_model()"]
T -->|kwargs| TeamInit["Team.__init__()"]
M -->|kwargs| AgentInit["Agent.__init__()"]
agent.leader_model 指定主 Team 使用的模型角色别名(如 default、flash、vision),映射到 config.yml 的 model_map 节中定义的实际模型。
agent.team 控制主 Team(Leader)行为,关键参数包括:
| 参数 | 默认值 | 说明 |
|---|---|---|
add_history_to_context | true | 是否注入历史对话 |
num_history_runs | 10 | 历史轮数 |
add_datetime_to_context | true | 时间感知(穿搭推理核心依赖) |
timezone_identifier | "Asia/Shanghai" | 时区 |
retries / exponential_backoff | 2 / true | LLM 调用容错 |
cache_session | true | 降低 DB 读取延迟 |
store_events / store_member_responses | true / true | AgentOS 追踪与 QC 审计 |
add_session_state_to_context | true | Leader 感知 last_agent_id 路由状态 |
enable_session_summaries | false | 多轮上下文压缩(未开启) |
show_members_responses | false | 开发调试用 |
add_team_history_to_members | false | 是否将 Team 历史下发给子 Agent |
agent.member 控制子 Agent 默认行为,参数结构与 team 一致,但独立配置。
Sources: config.yml, app/config.py
TOML 配置文件
项目使用 TOML 格式定义 Agent 团队、技能和 Canvas Agent 的结构化配置。这些文件由对应的 Loader 类在启动时加载,并支持灰度覆盖。
主 Team 配置:data/team/main.toml
定义主 Agent 的身份、工具列表、技能绑定和渠道专属工具:
graph LR
TOML["data/team/main.toml"] --> Loader["MainTeamLoader"]
Loader --> Config["MainTeamConfig"]
Config --> Tools["工具列表(全渠道 + 渠道专属)"]
Config --> Skills["技能绑定"]
Config --> Instructions["提示词路径"]
| 字段 | 类型 | 说明 |
|---|---|---|
id | str | Team 唯一标识符 |
name | str | 显示名称 |
tools | list[str] | 全渠道共用的基础工具名列表 |
skills | list[str] | 可用技能 ID 列表,引用 data/skills/*/skill.toml |
instructions.zh / .en | str | 中英文提示词文件路径(相对于 data/prompts/) |
[channels.<name>].tools | list[str] | 渠道专属工具,仅在对应渠道下可用 |
主 Team 配置支持灰度覆盖——通过 get_gray_config_service().get_toml_overlay() 可为特定用户提供不同的 TOML 内容。
Sources: data/team/main.toml, app/agents/team_loader.py
Canvas Agent 配置:data/team/canvas.toml
定义小程序 Canvas 专用 Agent 的参数:
| 字段 | 类型 | 说明 |
|---|---|---|
id | str | Agent 标识符 |
name | str | 显示名称 |
model | str | 模型角色别名(如 "vision") |
role_key | str | 角色描述的 i18n 键名 |
add_history_to_context | bool | 是否注入历史 |
num_history_runs | int | 历史轮数 |
tool_call_limit | int | 单轮最大工具调用次数 |
retries | int | LLM 重试次数 |
instructions.zh / .en | str | 提示词路径 |
Sources: data/team/canvas.toml
技能配置:data/skills/*/skill.toml
每个技能目录下包含一个 skill.toml 文件,定义技能的元数据、工具绑定和提示词路径:
graph TD
subgraph "data/skills/"
S1["nuva-soul/skill.toml"]
S2["style-advisor-skill/skill.toml"]
S3["buyer-wardrobe-skill/skill.toml"]
S4["outfit-output-skill/skill.toml"]
end
S1 & S2 & S3 & S4 --> Registry["SkillRegistry"]
Registry --> Bundle["SkillBundle"]
Bundle --> Agent["运行时 Agent"]
技能 TOML 的标准字段:
| 字段 | 必需 | 说明 |
|---|---|---|
id | ✅ | 技能唯一标识符 |
name | ✅ | 显示名称 |
description | ✅ | 技能用途描述,用于 Agent 判断何时加载 |
enable | ❌ | 是否启用(默认 true),设为 false 则跳过注册 |
activation | ❌ | 激活方式:"on_demand"(按需加载) |
tools | ❌ | 启用该技能时自动注入的工具名列表 |
allowed_tools | ❌ | 声明该技能推荐使用的工具边界(用于门控) |
[channels.<name>].tools | ❌ | 渠道专属工具覆盖 |
compatibility | ❌ | 兼容性声明 |
license | ❌ | 许可标识 |
[metadata] | ❌ | category、owner 等分类元数据 |
[instructions].zh / .en | ❌ | 提示词文件路径 |
当前项目注册了四个技能:
| 技能 ID | 分类 | 工具绑定 |
|---|---|---|
nuva-soul | persona(人格) | 无(纯提示词技能) |
style-advisor-skill | outfit(穿搭建议) | 无 |
buyer-wardrobe-skill | buyer-wardrobe(搜品/衣橱) | 18 个工具(搜索、链接、衣橱管理) |
outfit-output-skill | output(视觉输出) | 5 个工具 + 渠道专属覆盖 |
技能配置同样支持灰度覆盖,灰度范围以 data/skills/<id>/skill.toml 为 scope。
Sources: data/skills/nuva-soul/skill.toml, data/skills/buyer-wardrobe-skill/skill.toml, data/skills/outfit-output-skill/skill.toml, data/skills/example/skill.toml, app/skills/loader.py
工具描述文件:data/tools/descriptions.toml
工具描述文件以 TOML 格式集中管理所有工具的自然语言描述。这些描述会在运行时注入到 agno.tools.Function 对象上,作为 LLM 决策工具选择的核心依据。
文件结构为 [tools] 节下的键值对,键为工具名,值为多行描述文本。描述中包含使用场景、参数约束、返回值格式和与其他工具的边界说明。
该文件同样支持灰度覆盖——通过 user_id 参数加载用户专属的描述变体,materialize_tools_for_user() 函数负责合并基础描述与用户覆盖。
Sources: data/tools/descriptions.toml, app/tools/factory.py
提示词文件:data/prompts/
提示词文件以 .txt 纯文本格式存放在 data/prompts/ 目录下,按业务域组织子目录,所有文件均提供中英文双版本(.zh.txt / .en.txt):
| 子目录 | 内容 |
|---|---|
agents/ | Agent 和 Team 的系统指令(main_team、canvas_agent 等) |
skills/ | 各技能的加载后注入指令 |
memory/ | Mem0 记忆提取和洞察分析的提示词 |
workflows/ | 工作流适配器和生图测试提示词 |
partials/ | 可复用的提示词片段(如工具运行时护栏) |
system/ | 系统级提示词(快速响应、意图判断) |
| 根目录 | 业务专项提示词(穿搭总结、风格语气、头像提取等) |
提示词文件支持动态占位符:{category_taxonomy_yaml} 和 {category_taxonomy} 会在加载时被替换为实际的品类分类数据。
提示词加载还支持渠道覆盖——data/channels/<channel_name>/ 下的同名文件会覆盖全局提示词,实现不同渠道的差异化指令。加载顺序为:渠道覆盖 → Agent 本地 → Biz 默认。
Sources: data/prompts/agents/main_team.zh.txt, app/i18n.py, app/channel_prompts.py
国际化数据:data/i18n/
JSON 格式的国际化字符串文件,提供运行时文本的中英文对照:
| 文件 | 用途 |
|---|---|
system.*.json | Agent 描述、工具返回消息、准入控制文案 |
agent_instructions.*.json | Agent 指令文本 |
content_audit.*.json | 内容审核相关文案 |
inbound_shortcuts.*.json | 入站快捷指令文案 |
user_profile.*.json | 用户画像相关文案 |
weather.*.json | 天气查询返回文本 |
wechat.*.json | 微信渠道特定文案 |
workflow.*.json | 工作流相关文案 |
这些文件通过 app.i18n.get_text() 按键路径读取,语言参数默认为 "zh"。
Sources: data/i18n/system.zh.json, app/i18n.py
配置加载机制
配置加载的核心在 app.config 模块中实现,关键流程如下:
flowchart TD
A["启动 / get_settings()"] --> B["get_biz_settings()"]
B --> C["加载 outfit_biz/config.yml"]
B --> D["加载 .env"]
B --> E["读取环境变量"]
C --> F["_deep_merge(biz_yml, agent_yml)"]
G["加载 outfit_agent/config.yml"] --> F
F --> H["_merged_yml 全局字典"]
H --> I["AgentSettings(**biz.model_dump())"]
I --> J["@lru_cache 单例"]
合并策略:
- 1.
outfit_biz/config.yml提供基础配置 - 2.
outfit_agent/config.yml递归覆盖——同名标量直接替换,嵌套字典递归合并 - 3. 环境变量和
.env通过 PydanticBaseSettings注入,优先级最高 - 4.
AgentSettings继承BizSettings,添加 Agent 专属字段
AgentSettings 中的属性访问器(如 team_params、member_params、leader_model)直接从 _merged_yml 读取对应的嵌套字典,透传给 Team.__init__() 和 Agent.__init__()。
Sources: app/config.py
灰度配置
项目支持用户级别的配置灰度覆盖,通过 outfit_biz.gray.GrayConfigService 实现。灰度覆盖可作用于:
- - TOML 配置:为特定用户提供不同的
main.toml或skill.toml内容 - - 工具描述:为特定用户提供定制化的工具描述文本
- - 提示词:为特定用户提供差异化的提示词
灰度签名基于 user_id 和 scope 构建,相同签名的用户共享同一份配置变体缓存。未命中灰度规则的用户使用 base 配置。
Sources: app/agents/team_loader.py, app/skills/loader.py, app/tools/factory.py
配置文件全景图
以下树形结构展示项目中所有配置相关文件的组织方式:
outfit_agent/
├── .env.example # 环境变量模板
├── config.yml # Agent 层 YAML 主配置(叠加在 biz 层之上)
│
├── data/
│ ├── team/
│ │ ├── main.toml # 主 Team 配置(工具、技能、渠道工具)
│ │ └── canvas.toml # Canvas Agent 配置
│ │
│ ├── skills/
│ │ ├── nuva-soul/skill.toml # 人格技能
│ │ ├── style-advisor-skill/ # 穿搭建议技能
│ │ ├── buyer-wardrobe-skill/ # 搜品衣橱技能
│ │ └── outfit-output-skill/ # 视觉输出技能
│ │
│ ├── tools/
│ │ └── descriptions.toml # 工具描述集中管理
│ │
│ ├── prompts/ # 提示词文件(.txt,中英双版本)
│ │ ├── agents/ # Agent 系统指令
│ │ ├── skills/ # 技能指令
│ │ ├── memory/ # 记忆/洞察提示词
│ │ ├── workflows/ # 工作流提示词
│ │ ├── partials/ # 可复用片段
│ │ └── system/ # 系统级提示词
│ │
│ ├── i18n/ # 国际化 JSON 字符串
│ │ ├── system.zh.json
│ │ ├── system.en.json
│ │ └── ...
│ │
│ └── channels/ # 渠道覆盖文件
│ ├── wechat_kefu/ # 微信客服渠道覆盖
│ └── wechat_miniapp/ # 小程序渠道覆盖
│
└── app/
├── config.py # AgentSettings 定义与配置加载
├── i18n.py # 国际化加载与搜索路径注册
├── channel_prompts.py # 渠道提示词覆盖逻辑
├── agents/team_loader.py # 主 Team TOML 配置加载器
├── skills/loader.py # 技能 TOML 注册表
└── tools/factory.py # 工具描述加载与灰度合并
下一步
- - 如需了解环境变量的详细分类和用途,请参阅 环境变量配置
- - 如需了解配置如何驱动系统整体运行,请参阅 系统架构总览
- - 如需了解技能加载与管理的完整流程,请参阅 技能加载与管理
- - 如需了解工具描述管理与灰度机制,请参阅 工具描述管理
- - 如需了解提示词管理与国际化,请参阅 提示词管理与国际化
<a id="5-xi-tong-jia-gou-zong-lan"></a>
---
系统架构总览
- - Section: 架构与核心概念
- - Source File:
5-xi-tong-jia-gou-zong-lan.md
本页是 Outfit Agent 系统的全局架构导览,面向首次接触代码库的开发者。文章从分层架构、请求生命周期、核心子系统三个维度,帮助你建立对整个系统的宏观认知,并指引你进入各专题的深度阅读。
技术栈概览
Outfit Agent 是一个基于 FastAPI + Agno Agent 框架 构建的智能穿搭顾问 REST API 服务。下表汇总了系统的核心技术选型与职责:
| 层次 | 技术 | 职责 |
|---|---|---|
| Web 框架 | FastAPI + uvicorn | HTTP/WebSocket 路由,SSE 流式响应 |
| Agent 框架 | Agno (agno.agent.Agent) | LLM 编排、Tool 调用、会话持久化 |
| AgentOS | Agno AgentOS | OpenTelemetry 追踪、Cron 调度、MCP Server |
| 数据库 | PostgreSQL (asyncpg / psycopg2) | 业务数据、会话历史、AgentOS tracing |
| 向量存储 | pgvector | Mem0 记忆向量检索 |
| 缓存 / 队列 | Redis | 消息去重、会话队列、LiteLLM 缓存、实时 Run 状态 |
| 可观测性 | OpenTelemetry + HyperDX | 日志、链路追踪 |
| 模型调用 | LiteLLM → DashScope / OpenAI | LLM completion 与 embedding |
Sources: README.md, app/main.py, app/agentos.py, app/observability.py
分层架构
整个系统遵循清晰的分层结构,自上而下依次为:入口路由层 → 消息处理层 → Agent 层 → 工具层 → 服务层 → 数据/记忆层。
graph TB
subgraph 入口路由层
A1[微信客服路由<br/>routers/wechat.py]
A2[小程序路由<br/>routers/miniapp/*]
A3[REST Chat 路由<br/>routers/chat.py]
A4[Admin API<br/>routers/admin/*]
A5[Canvas WS 路由<br/>routers/miniapp/canvas_ws.py]
end
subgraph 消息处理层
B1[消息入站归一化<br/>messaging/inbound.py]
B2[会话分发状态机<br/>messaging/session_dispatcher.py]
B3[Agent 执行服务<br/>messaging/agent_service.py]
B4[回复状态管理<br/>messaging/reply_state.py]
B5[快速响应<br/>fast_response.py]
end
subgraph Agent 层
C1[主 Agent 单例<br/>agent.py]
C2[Canvas Agent 单例<br/>canvas/agent.py]
C3[Team 配置加载<br/>agents/team_loader.py]
C4[技能注册表<br/>skills/loader.py]
C5[Dialog State<br/>dialog_state.py]
end
subgraph 工具层
D1[工具工厂<br/>tools/factory.py]
D2[工具注册表<br/>tools/__init__.py]
D3[ToolContext<br/>tools/context.py]
D4[业务工具集<br/>tools/*.py]
end
subgraph 服务层
E1[穿搭工作流<br/>workflows/outfit_generation.py]
E2[图片生成<br/>img_gen_handler.py]
E3[单品初始化<br/>item_init_handler.py]
E4[图片描述<br/>image_caption_handler.py]
E5[Outfit 服务<br/>services/outfit_*.py]
end
subgraph 数据与记忆层
F1[(PostgreSQL)]
F2[(Redis)]
F3[Mem0 向量记忆<br/>memory/client.py]
F4[用户洞察<br/>insights/service.py]
end
A1 & A2 & A3 & A5 --> B1
A4 -.-> |admin 直连| F1
B1 --> B2 --> B3
B3 --> C1 & C2
B4 & B5 -.-> B3
C1 --> C3 --> C4
C1 & C2 --> C5
C1 & C2 --> D1 --> D2 --> D4
D1 --> D3
D4 --> E1 & E2 & E3 & E5
E2 & E3 & E4 --> F2
C1 & C2 --> F1
C1 --> F3
F4 --> F3
E1 --> F1
Sources: app/main.py, app/tools/__init__.py, app/messaging/session_dispatcher.py
应用启动流程
系统启动时通过 _build_app() 函数完成一系列初始化,最终返回一个融合了业务路由与 AgentOS 路由的 ASGI 应用。启动顺序如下:
sequenceDiagram
participant main as main.py
participant agent as Agent 初始化
participant app as FastAPI
participant agentos as AgentOS
participant lifespan as Lifespan
main->>main: load_dotenv(.env)
main->>main: _init_httpx_client()
main->>agent: init_agent() — 构建主 Agent 单例
main->>agent: init_canvas_agent() — 构建 Canvas Agent 单例
main->>main: init_hyperdx_monitoring()
main->>app: 创建 FastAPI 实例 + 注册路由
main->>agentos: get_enriched_app(base_app, lifespan)
Note over agentos: AgentOS 挂载 tracing / scheduler / MCP
Note over lifespan: ← 进入 lifespan
lifespan->>lifespan: DB schema migration (users/outfits/items)
lifespan->>lifespan: ensure_gray_config_schema()
lifespan->>lifespan: ensure_insight_schema()
lifespan->>lifespan: ensure_message_access_control_schema()
lifespan->>lifespan: init_memory() — Mem0 AsyncMemory 单例
lifespan->>lifespan: _init_litellm_redis_cache()
lifespan->>lifespan: recover_interrupted_workflows()
lifespan->>lifespan: ImgGenHandler.start()
lifespan->>lifespan: ImageCaptionHandler.start()
lifespan->>lifespan: ItemProcessingHandler.start()
Note over lifespan: ← yield — 服务就绪
Sources: app/main.py, app/main.py, app/agentos.py
双 Agent 架构
系统运行时存在两个独立的 Agent 单例,各自服务不同的交互场景:
| Agent | 配置来源 | 职责 | 典型入口 |
|---|---|---|---|
主 Agent (outfit-main-team) | data/team/main.toml | 通用聊天、穿搭请求、用户画像管理、SOP 流程驱动 | /api/chat、微信客服、小程序 WS |
Canvas Agent (canvas-agent) | data/team/canvas.toml | 穿搭画布编辑——搜索单品、替换单品、场景编辑 | Canvas WebSocket |
两者共享同一个 PostgreSQL 数据库(schema=agno)和同一套工具注册表,但拥有独立的 prompt 指令、工具列表和 session state 隔离。Canvas Agent 通过 canvas/bridge.py 将编辑摘要桥接回主对话上下文。
Sources: app/agent.py, app/canvas/agent.py, data/team/main.toml, app/canvas/bridge.py
消息处理生命周期
一条用户消息从进入系统到 Agent 回复,经历以下核心阶段:
flowchart TD
A[用户消息到达] --> B{渠道识别}
B -->|微信客服| C[WXBizMsgCrypt 解密]
B -->|小程序| D[Token 鉴权 / WebSocket]
B -->|REST API| E[LoginRequired 校验]
C & D & E --> F[消息入站归一化<br/>messaging/inbound.py]
F --> G[入站快捷回复检查<br/>inbound_shortcuts.py]
G -->|命中快捷回复| H[直接返回固定回复]
G -->|未命中| I[内容审核<br/>aliyun_text_audit.py]
I --> J{会话分发状态机<br/>session_dispatcher.py}
J -->|图片消息| K[标记 image_in_flight]
J -->|文本消息| L[文本任务调度]
K --> M[图片处理管线<br/>image_pipeline.py]
M --> N[图片描述 / 存储]
N --> O[合并到 Agent 输入]
L --> P{检查任务冲突}
P -->|可取消| Q[合并待处理消息]
P -->|需等待| R[入 Redis 队列排队]
Q & O --> S[Agent Turn 执行<br/>agent_service.py]
S --> T[build_tool_ctx → ToolContext]
T --> U[Agent.arun]
U --> V[LLM 调用 + Tool 调用链]
V --> W[回复过滤<br/>display_filters.py]
W --> X[持久化回复消息]
X --> Y[渠道投递<br/>send_text_message]
Sources: app/routers/wechat.py, app/routers/chat.py, app/messaging/session_dispatcher.py, app/messaging/agent_service.py
工具系统
工具(Tool)是 Agent 与外部世界交互的桥梁。系统通过 工厂模式 + 注册表模式 管理所有工具:
工具工厂 (tools/factory.py) 负责:将普通 Python 函数包装为 Agno Function 对象;在运行时根据用户 ID 动态加载 TOML 格式的工具描述覆盖;以及注入工具门控逻辑(Dialog State 检查、渠道权限检查、技能绑定检查)。
工具注册表 (tools/__init__.py) 是一个全局 dict[str, Function],包含以下工具类别:
| 工具类别 | 工具函数示例 | 职责 |
|---|---|---|
| 穿搭工作流 | run_outfit_flow | 启动异步穿搭生成流程 |
| 单品管理 | search_items, save_item_batch, update_item_tool | 衣柜单品 CRUD |
| 穿搭管理 | generate_outfit, edit_outfit, search_outfits_tool | 穿搭作品管理 |
| 图片生成 | generate_image, generate_tryon_tool | AI 图片生成与虚拟试穿 |
| 图片搜索 | search_images_tool | 以图搜图 |
| 消息投递 | send_text_message, send_outfits, send_miniprogram_message | 跨渠道消息发送 |
| Canvas | search_canvas_items, save_outfit_from | 画布编辑操作 |
| 用户 | update_user_profile, new_session | 用户画像与会话管理 |
| 对话状态 | get_dialog_state, update_dialog_state | SOP 流程状态读写 |
| 天气 | query_weather_now, query_weather_forecast | 和风天气 API |
| 购物 | get_item_links_tool, get_cheapest_link_tool | 商品链接查询 |
ToolContext (tools/context.py) 是一个会话级别的上下文对象,存储在 Agno session_state["tool_ctx"] 中,包含 user_id、lang、session_id、avatar_path、channel 等信息,所有业务工具通过 _get_ctx(run_context) 提取,避免依赖 LLM 传递用户身份参数。
Sources: app/tools/factory.py, app/tools/__init__.py, app/tools/context.py
技能系统
技能(Skill)是组织工具和指令的业务单元。每个技能由 data/skills/<id>/skill.toml 声明,包含:绑定的工具列表、渠道专属工具、激活条件、参考文档和脚本。
SkillRegistry (skills/loader.py) 在启动时扫描 data/skills/ 目录,构建 RegisteredSkill 对象。主 Agent 的 data/team/main.toml 通过 skills 列表引用技能 ID:
skills = [
"nuva-soul",
"style-advisor-skill",
"buyer-wardrobe-skill",
"outfit-output-skill",
]
技能的工具通过 门控机制 控制可见性:当 active_skill 字段匹配技能 ID 时,该技能的工具才对 Agent 可用。这确保 Agent 在不同 SOP 阶段只暴露相关工具,降低 LLM 决策复杂度。
Sources: app/skills/loader.py, data/team/main.toml
Dialog State 与 SOP 流程
Dialog State 是嵌入在 Agno session_state["dialog_state"] 中的有限状态机,驱动整个对话的 SOP(标准操作流程)推进。其核心字段包括:
| 字段 | 作用 |
|---|---|
active_skill | 当前激活的技能 ID,控制工具门控 |
sop_stage | SOP 当前阶段(如 collect_preferences、await_outfit_confirmation) |
collected_slots | 已收集的用户偏好槽位 |
outfit_status | 穿搭工作流状态(running、awaiting_confirmation、completed) |
pending_confirmation | 待用户确认的动作定义 |
active_scene | 当前场景标识 |
Agent 通过 update_dialog_state 工具推进状态,get_dialog_state 工具读取状态。工具门控函数 is_tool_allowed() 根据 active_skill 决定工具是否可用。
Sources: app/dialog_state.py
会话分发状态机
当用户快速连续发送多条消息时,会话分发器 (messaging/session_dispatcher.py) 通过模块级状态字典确保消息的顺序处理:
- -
_text_tasks:每个 session 最多一个正在运行的 Agent 文本任务 - -
_image_processing:图片消息处理独占锁——图片处理期间阻塞所有后续消息 - - 消息合并:可取消的待处理文本消息会合并到下一条消息中(debounce 行为)
- - 队列排空:任务完成后自动调用
drain_queue()处理队列中的下一条消息
渠道特定路由(如微信客服)只需提供 on_text 和 prepare_image 两个回调,即可接入统一的分发状态机。
Sources: app/messaging/session_dispatcher.py
穿搭工作流
穿搭生成采用 异步工作流架构,通过 Agno Workflow 实现多步骤编排:
flowchart LR
A[prepare_context] --> B[ideate_outfits]
B --> C[select_items]
C --> D[generate_images]
D --> E[dispatch_results]
style A fill:#e8f5e9
style B fill:#e3f2fd
style C fill:#fff3e0
style D fill:#fce4ec
style E fill:#f3e5f5
工作流通过 OutfitWorkflowService 以 asyncio.create_task 在后台运行。步骤 B 由 OutfitIdeationService 调用 LLM 生成穿搭创意;步骤 C 由 OutfitItemSelectionService 从用户衣柜中匹配单品;步骤 D 将任务入 Redis 队列,由后台 ImgGenHandler 消费并调用图片生成 API。
背景任务处理器是一个统一模式:Redis 队列负责任务调度,数据库 reconcile 保障幂等性,processing lock 防止重复消费。系统中有三个此类处理器:
| 处理器 | 职责 | 配置键前缀 |
|---|---|---|
ImgGenHandler | 穿搭图片生成 | img_gen_* |
ImageCaptionHandler | 单品图片描述生成 | image_caption_* |
ItemProcessingHandler | 单品属性初始化 | item_init_* |
Sources: app/workflows/outfit_generation.py, app/services/outfit_workflow_service.py, app/img_gen_handler.py, app/main.py
记忆与洞察
Mem0 记忆 (memory/) 提供基于 pgvector 的用户长期记忆。系统在 Agent 每轮对话前检索相关记忆(search_user_memory),在对话后提取并持久化新记忆(add_conversation_memory)。配置项 mem0_realtime_analysis_enabled 控制是否启用逐轮实时记忆提取。
用户洞察 (insights/) 是离线分析管线,通过 Agno Cron Scheduler 每日执行,分析用户的穿搭数据(outfit)、单品数据(item)和聊天数据(chat),生成结构化洞察并写入 user_insights 表。洞察结果可同步回 Mem0 记忆层。
Sources: app/memory/client.py, app/memory/__init__.py, app/insights/service.py
渠道抽象
系统支持多个接入渠道,通过 渠道名 统一抽象:
| 渠道标识 | 路由文件 | 交互方式 |
|---|---|---|
wechat_kefu | routers/wechat.py | 企业微信客服回调(XML + 加密) |
wechat_miniapp | routers/miniapp/chat.py, ws.py | 小程序 REST + WebSocket |
wechat_miniapp (Canvas) | routers/miniapp/canvas_ws.py | 画布专用 WebSocket |
api | routers/chat.py | REST API(POST /api/chat,支持 SSE) |
渠道差异通过 data/team/main.toml 中的 [channels.*] 段管理工具可见性,以及 data/i18n/ 和 data/prompts/ 下的文件覆盖实现 prompt 国际化。ToolContext.channel 字段贯穿整个调用链,使工具层能够感知当前渠道并做出差异化行为。
Sources: data/team/main.toml, app/tools/factory.py, app/routers/wechat.py
配置与灰度
配置采用 双层 YAML 合并 策略:outfit_biz/config.yml 作为基础配置,outfit_agent/config.yml 作为覆盖层,通过深度递归合并生成最终配置。AgentSettings 继承自 BizSettings,增加了 Agent 专有字段(如微信配置、Mem0 参数、后台处理器并发数等)。
灰度系统 支持按用户 ID 对配置文件(包括 team TOML、skill TOML、tool descriptions TOML)施加覆盖,实现 A/B 测试。各加载器(MainTeamLoader、SkillRegistry、_load_tool_descriptions)均内置了灰度签名计算和缓存。
Sources: app/config.py
管理与可观测性
- - Admin API (
routers/admin/):提供 20+ 个管理端点,涵盖用户管理、Agent 配置热更新、技能管理、灰度规则、Prompt 编辑、链路追踪查看、Mem0 记忆管理等 - - Dashboard:SPA 应用,构建产物挂载在
/dashboard/路径下 - - Live Runs (
live_runs.py):将当前正在执行的 Agent Run 实时写入 Redis,供 Dashboard 监控 - - HyperDX:通过 OpenTelemetry OTLP 协议将日志导出到 HyperDX 平台
- - AgentOS Tracing:通过 OpenTelemetry + DatabaseSpanExporter 将 Agent 链路追踪数据写入 PostgreSQL(schema=agno)
Sources: app/live_runs.py, app/observability.py, app/agentos.py
项目目录结构
outfit_agent/
├── app/ # 应用核心代码
│ ├── main.py # FastAPI 入口 + lifespan 管理
│ ├── agent.py # 主 Agent 单例构建与运行
│ ├── agentos.py # AgentOS 集成(tracing/scheduler/MCP)
│ ├── config.py # 配置加载与合并
│ ├── dialog_state.py # Dialog State 有限状态机
│ ├── session.py # 会话 ID 管理
│ ├── i18n.py # 国际化桥接
│ ├── fast_response.py # 快速响应机制
│ ├── agents/ # Agent 配置加载(team_loader, profile_hook)
│ ├── canvas/ # Canvas Agent(agent, bridge, context, service)
│ ├── insights/ # 离线用户洞察管线
│ ├── memory/ # Mem0 记忆集成
│ ├── messaging/ # 消息处理(inbound, dispatcher, agent_service)
│ ├── routers/ # HTTP/WebSocket 路由
│ │ ├── chat.py # REST Chat API
│ │ ├── wechat.py # 微信客服回调
│ │ ├── miniapp/ # 小程序路由集
│ │ └── admin/ # Admin API 端点集
│ ├── services/ # 业务服务层
│ ├── skills/ # 技能加载与注册
│ ├── tools/ # 工具工厂、注册表与业务工具
│ └── workflows/ # 穿搭生成工作流
├── data/ # 非代码资源
│ ├── team/ # Agent/Team TOML 配置
│ ├── skills/ # 技能定义目录
│ ├── prompts/ # Prompt 模板
│ ├── i18n/ # 国际化 JSON
│ └── tools/ # 工具描述 TOML
├── dashboard/ # 管理面板 SPA
├── tests/ # 测试套件
├── scripts/ # 运维脚本
├── config.yml # Agent 层配置覆盖
└── pyproject.toml # 项目元数据与依赖
推荐阅读路径
基于以上架构全景,建议按以下顺序深入各专题:
- 1. 单主 Agent 架构 — 理解 Agent 单例构建、模型解析、运行时变体机制
- 2. 技能加载与管理 — 掌握 Skill 注册表、TOML 配置与灰度机制
- 3. Dialog State 设计 — 理解 SOP 流程如何驱动对话状态推进
- 4. 工具注册与工厂 — 深入工具包装、门控、描述管理
- 5. 消息入站处理流程 — 从用户消息到 Agent 调用的完整链路
- 6. 穿搭工作流服务 — 异步工作流编排与图片生成管线
<a id="6-dan-zhu-agent-jia-gou"></a>
---
单主 Agent 架构
- - Section: 架构与核心概念
- - Group: Agent 与技能系统
- - Source File:
6-dan-zhu-agent-jia-gou.md
本文档详细介绍了 Outfit Agent 项目采用的单主 Agent 架构设计。该架构以 agno 框架为基础,采用单一 Agent 作为核心业务编排者,通过动态技能加载和显式 SOP 状态推进,实现了高效、可控的智能穿搭顾问系统。
架构设计原则
单主 Agent 架构的核心设计理念源于对传统多 Agent 协作模式的反思与优化。在早期架构中,系统采用"主 Team 协调多个子 agent"的模式,但这种模式存在几个关键问题:场景容易被整体交给某个子 agent 处理、系统容易自动跑完整长链路而缺乏中间确认点、会话状态管理不够显式。
基于这些挑战,当前架构遵循以下核心原则:一轮一个主目标——每轮交互只处理一个主要意图;一步一个动作——避免在单次调用中执行过多操作;关键动作前先确认——在重要决策点暂停等待用户反馈;当前动作完成后及时收口——确保每次交互都有明确的结束状态。这些原则共同保证了系统行为的可预测性和用户体验的可控性。
Sources: app/agent.py, docs/agno_agents.md, docs/agent_refact.md
核心组件架构
单主 Agent 架构由三个核心组件构成:主 Agent 实例、技能注册系统和工具工厂系统。这三个组件协同工作,形成了完整的业务处理流程。
主 Agent 实例是整个系统的唯一业务编排者,由 app/agent.py 中的 init_agent() 函数初始化并缓存为全局单例。该 Agent 承担意图识别、技能加载决策、SOP 阶段推进和最终回复生成等核心职责。Agent 实例通过 _build_agent() 函数构建,该函数整合了模型配置、团队配置、技能包和工具列表,最终创建一个可复用的 Agent 对象。
技能注册系统由 app/skills/loader.py 中的 SkillRegistry 类实现。该系统负责扫描 data/skills/ 目录下的技能定义文件(skill.toml),构建技能对象并注册到全局注册表中。每个技能包含激活条件、指令提示词、工具列表和访问控制规则等元数据。技能的激活采用"按需加载"模式,只有当主 Agent 识别到特定意图时才会调用相应的技能指令。
工具工厂系统由 app/tools/factory.py 中的 build_tool() 函数和 TOOL_REGISTRY 字典共同构成。该系统负责将业务函数包装为 agno 可识别的 Function 对象,并注入必要的上下文(用户信息、会话状态、渠道数据等)。工具描述从 data/tools/descriptions.toml 文件加载,支持灰度配置覆盖。
Sources: app/agent.py, app/skills/loader.py, app/tools/factory.py
单例初始化与生命周期管理
主 Agent 的初始化遵循严格的启动序列,确保所有依赖组件在使用前已正确配置。整个初始化过程由 init_agent() 函数协调,该函数在应用启动时被调用一次。
sequenceDiagram
participant Main as app/main.py
participant Agent as app/agent.py
participant Tools as app/tools
participant Skills as app/skills
participant Loader as team_loader.py
Main->>Agent: init_agent()
Agent->>Agent: _make_agent_db()
Agent->>Agent: _make_agent_sync_engine()
Agent->>Tools: init_tool_registry()
Agent->>Skills: init_skill_registry()
Agent->>Loader: get_main_team_loader().reload()
Agent->>Agent: _resolve_model_runtime(leader_model)
Agent->>Agent: _build_agent(leader_runtime)
Agent-->>Main: Agent singleton ready
初始化完成后,Agent 实例被缓存在 _agent 全局变量中,并通过 get_agent() 函数提供给其他模块使用。该函数支持基于用户 ID 的灰度变体缓存,确保不同用户群体可以使用不同的 Agent 配置而互不干扰。
Agent 的生命周期与应用进程绑定,支持热重载功能。当配置文件或技能定义发生变化时,可以通过调用 reload_agents() 函数重新构建 Agent 实例,无需重启整个应用。热重载过程会依次重新初始化工具注册表、技能注册表和团队加载器,然后重新构建 Agent 实例。
Sources: app/agent.py, app/main.py
配置驱动的团队定义
主 Agent 的行为由 data/team/main.toml 配置文件定义,该文件采用 TOML 格式,清晰地声明了 Agent 的工具、技能和指令集。这种配置驱动的设计使得系统行为可以在不修改代码的情况下进行调整。
配置文件的主要结构包括:
- -
id和name:Agent 的唯一标识和显示名称 - -
tools:主 Agent 常驻可用的基础工具列表 - -
skills:主 Agent 可用的技能列表 - -
instructions:多语言指令提示词路径 - -
channels:渠道专属工具配置
例如,当前配置声明了 run_outfit_flow、update_user_profile、new_session 等基础工具,以及 nuva-soul、style-advisor-skill、buyer-wardrobe-skill、outfit-output-skill 等核心技能。渠道配置允许为特定渠道(如微信客服)声明专属工具,实现场景化的功能控制。
配置加载由 MainTeamLoader 类负责,该类支持灰度配置覆盖。当指定了用户 ID 时,加载器会查询灰度配置服务,根据用户所属的灰度组应用不同的配置覆盖,从而实现 A/B 测试和渐进式发布。
Sources: data/team/main.toml, app/agents/team_loader.py
技能激活与工具门控机制
技能激活是单主 Agent 架构的核心动态行为。当主 Agent 识别到用户意图需要特定技能时,会通过 get_skill_instructions 工具调用加载相应的技能指令。这个过程同时会更新对话状态中的 active_skill 字段,触发工具门控机制。
工具门控机制由 app/dialog_state.py 中的 is_tool_allowed() 函数实现。该函数检查当前工具是否属于某个技能,以及该技能是否已被激活。如果工具属于某个技能但该技能未被激活,工具调用将被拒绝并返回错误信息。这种机制确保了工具只能在正确的上下文中使用,避免了越权操作。
flowchart TD
A[用户消息] --> B[主Agent意图识别]
B --> C{需要特定技能?}
C -->|是| D[调用get_skill_instructions]
D --> E[更新dialog_state.active_skill]
E --> F[技能指令加载完成]
C -->|否| G[使用主Agent内建能力]
F --> H[工具门控检查]
G --> H
H --> I{工具是否允许?}
I -->|是| J[执行工具]
I -->|否| K[返回错误信息]
J --> L[更新对话状态]
L --> M[生成用户回复]
技能激活还支持"always instruction"模式,某些技能的核心指令会被注入到主 Agent 的基础指令中,确保这些技能的行为准则始终生效。例如,nuva-soul 技能的人格定义指令就采用这种模式,保证主 Agent 在任何交互中都保持一致的人格特征。
Sources: app/skills/access_tools.py, app/dialog_state.py, app/tools/factory.py
会话状态与对话管理
会话状态管理是单主 Agent 架构保持对话连贯性的关键。系统采用分层状态设计:session_state 存储会话级数据,dialog_state 存储对话流程状态,ToolContext 存储请求级上下文。
dialog_state 是对话流程管理的核心数据结构,由 app/dialog_state.py 定义。它包含以下关键字段:
- -
active_skill:当前激活的技能 ID - -
loaded_skills:已加载的技能列表 - -
sop_stage:当前 SOP 阶段 - -
active_scene:当前活跃场景 - -
outfit_task:穿搭任务配置 - -
outfit_status:穿搭生成状态 - -
pending_confirmation:待用户确认的内容
这些字段共同构成了对话流程的状态机,主 Agent 根据这些状态决定下一步行为。例如,当 sop_stage 为 await_outfit_confirmation 时,Agent 会等待用户确认而不是继续执行下一步操作。
ToolContext 是请求级上下文,包含用户 ID、语言偏好、渠道信息、头像路径等数据。它通过 set_session_tool_ctx() 函数注入到 session_state 中,并在工具调用时通过 _get_ctx() 函数提取。这种设计确保了每个工具调用都能获得正确的上下文信息。
Sources: app/dialog_state.py, app/tools/context.py
消息处理与并发控制
消息处理采用会话级并发控制机制,确保每个会话同时只有一个 Agent 调用在执行。该机制由 app/messaging/session_dispatcher.py 实现,采用状态机模式管理文本和图片消息的处理顺序。
状态机的核心状态包括:
- -
_text_tasks:当前正在执行的文本处理任务 - -
_image_processing:正在处理的图片任务 - -
_queue_draining:正在排空队列的会话
当用户发送文本消息时,系统首先检查是否有图片正在处理。如果有,文本消息会被加入待处理队列;如果没有,则检查是否有正在执行的文本任务。如果存在文本任务且新消息可以合并(防抖行为),则取消当前任务并将新消息合并到队列中;否则直接创建新任务执行。
图片消息的处理具有更高的优先级。一旦图片开始处理,所有后续消息(包括文本)都会被阻塞,直到图片处理完成。这种设计确保了图片处理流程的完整性,避免了图片处理过程中被其他消息干扰。
Sources: app/messaging/session_dispatcher.py
内存与上下文增强
系统集成了 Mem0 记忆模块,在每次 Agent 调用前检索相关记忆,为对话提供个性化上下文。记忆检索由 app/memory 模块的 search_user_memory() 函数实现,该函数基于语义相似度搜索用户的历史对话和记忆片段。
记忆上下文的注入遵循"读取前、写入后"的模式:
- 1. 读取前:在构建 Agent 输入前,先检索与当前消息相关的记忆
- 2. 构建输入:将记忆上下文、运行时上下文和用户消息组合成完整的输入
- 3. 写入后:在获得 Agent 回复后,异步地将对话记录添加到记忆中
这种设计确保了记忆操作不会阻塞主处理流程,同时为 Agent 提供了丰富的历史上下文。记忆上下文的长度受配置限制,避免过长的记忆片段影响 Agent 的处理效率。
Sources: app/agent.py, app/memory/__init__.py
快速响应与用户体验优化
为了提升用户体验,系统实现了快速响应机制,在 Agent 处理耗时操作时向用户发送即时反馈。该机制由 app/fast_response.py 模块实现,采用"工具调用响应优先"策略。
快速响应的工作流程:
- 1. 在 Agent 调用开始时,注册快速响应请求
- 2. 启动异步任务监听第一个工具调用响应
- 3. 如果在指定超时时间内收到工具调用响应,立即将该响应发送给用户
- 4. 如果超时,则发送预设的 fallback 文本
这种机制特别适用于需要调用外部 API 的场景,如天气查询、图片生成等。用户可以在 Agent 完成完整处理前就获得初步反馈,显著降低了感知延迟。
快速响应还支持渠道特定的格式化。例如,微信客服渠道会解析菜单标记语法,将特定格式的文本转换为小程序卡片消息,提供更丰富的交互体验。
Sources: app/fast_response.py, app/agent.py
AgentOS 集成与监控
主 Agent 通过 app/agentos.py 模块集成到 Agno AgentOS 系统,获得完整的运维支持能力。AgentOS 提供了三大核心特性:追踪(Tracing)、调度(Scheduler)和 MCP Server。
追踪系统基于 OpenTelemetry 标准,将 Agent 的执行轨迹、工具调用和性能指标记录到 PostgreSQL 数据库。追踪数据存储在 agno_traces 和 agno_spans 表中,支持通过 Dashboard 或 Admin API 进行查询和分析。
调度系统支持 Cron 表达式定义的定时任务,可用于定期执行数据清理、统计计算等后台操作。调度任务通过 /v1/schedules API 进行管理。
MCP Server 提供了标准化的服务端点,允许外部系统通过 MCP 协议与 Agent 进行交互。
AgentOS 采用"base_app"模式挂载到现有的 FastAPI 应用上,保留所有业务路由的同时,添加 AgentOS 的管理路由。路由冲突处理策略为"preserve_base_app",确保业务路由优先级高于 AgentOS 路由。
Sources: app/agentos.py, app/main.py
热重载与配置更新
系统支持运行时热重载,允许在不重启服务的情况下更新 Agent 配置。热重载通过 reload_agents() 函数触发,该函数执行以下步骤:
- 1. 重新初始化工具注册表,加载最新的工具定义
- 2. 重新初始化技能注册表,扫描技能目录的变更
- 3. 重新加载团队配置文件
- 4. 清除 Agent 变体缓存
- 5. 使用新配置重新构建 Agent 实例
热重载过程是原子性的,在重新构建完成前,现有 Agent 实例继续处理请求。新实例构建完成后,通过全局变量替换实现无缝切换。这种设计确保了配置更新期间的服务连续性。
热重载还支持灰度配置的更新。当灰度配置发生变化时,相关用户的 Agent 变体会被重新构建,确保灰度策略的及时生效。
Sources: app/agent.py
错误处理与容错机制
系统实现了多层错误处理机制,确保在各种异常情况下都能提供合理的响应。
模型调用错误:Agent 配置了重试机制,当 LLM 调用失败时会自动重试。重试次数和超时时间通过配置参数控制。
工具执行错误:工具包装层捕获所有工具执行异常,记录详细的错误日志,并向 Agent 返回结构化的错误信息。Agent 可以根据错误类型决定是重试、跳过还是终止当前任务。
会话状态异常:当会话状态加载或持久化失败时,系统会降级到默认状态继续处理,确保对话不会因为状态异常而中断。
内存操作错误:记忆检索和写入操作采用异步执行模式,失败时只记录日志而不影响主处理流程。
所有错误都会记录到追踪系统中,支持后续的问题诊断和性能优化。
Sources: app/agent.py, app/tools/factory.py
性能优化策略
单主 Agent 架构采用了多项性能优化措施:
模型实例缓存:通过 _models 字典缓存已构建的模型实例,避免重复创建。模型实例的缓存键基于配置哈希生成,确保配置变更时能创建新的实例。
工具描述缓存:工具描述文件加载后会被缓存,支持用户级别的覆盖。缓存采用 LRU 策略,自动淘汰不常用的条目。
Agent 变体缓存:不同用户群体的 Agent 变体会被缓存,避免为每个请求都重新构建。缓存键包含模型配置和灰度签名的哈希值。
异步操作优化:记忆写入、指标上报等非关键路径操作采用异步执行,不阻塞主处理流程。
连接池管理:全局 HTTP 客户端配置了连接池,支持高并发场景下的连接复用。
这些优化措施共同确保了系统在高并发场景下的响应性能和资源利用率。
<a id="7-ji-neng-jia-zai-yu-guan-li"></a>
---
技能加载与管理
- - Section: 架构与核心概念
- - Group: Agent 与技能系统
- - Source File:
7-ji-neng-jia-zai-yu-guan-li.md
本文档详细说明 Outfit Agent 系统中技能(Skill)的定义、加载、注册与运行时管理机制。技能是系统实现"单主 Agent + 动态 Skills"架构的核心抽象,它将业务能力模块化,使主 Agent 能够按需加载不同的业务场景能力。
技能系统概述
Outfit Agent 采用单主 Agent + 动态技能架构,技能是该架构的核心组成部分。主 Agent 负责意图识别、SOP 推进和对话状态管理,而具体的业务能力(如搜品、穿搭输出、人格表达)则封装在独立的技能模块中。这种设计实现了业务逻辑与核心调度的解耦,使得新增业务能力只需添加技能定义文件,无需修改主 Agent 代码。
技能系统的核心职责包括:
- 1. 业务能力封装:每个技能定义一组工具集合、指令提示词和激活条件
- 2. 工具门控:技能拥有的工具只有在技能被激活后才能被调用
- 3. 渠道适配:支持为不同渠道(如微信客服、小程序)配置不同的工具集
- 4. 灰度发布:支持按用户 ID 对技能配置进行灰度覆盖
Sources: app/skills/loader.py, docs/agent_skill流程划分.md
技能定义文件结构
技能定义文件存放在 data/skills/ 目录下,支持两种布局方式:
目录式布局(推荐):
data/skills/<skill-id>/
├── skill.toml # 技能定义文件
├── references/ # 参考资料目录
│ └── guide.md
└── scripts/ # 脚本目录
└── helper.py
单文件布局:
data/skills/<skill-id>.toml
skill.toml 文件使用 TOML 格式定义技能的元数据、工具列表、激活条件等属性。以下是一个完整的技能定义示例:
id = "outfit-output-skill"
name = "Outfit Output Skill"
description = "负责最终视觉输出,包括生成穿搭、试穿和发送最终结果。"
activation = "on_demand"
compatibility = "outfit-agent>=1.0"
license = "internal"
tools = [
"generate_outfit",
"generate_image",
"generate_tryon_tool",
"send_outfits",
"send_image_message",
]
allowed_tools = [
"generate_outfit",
"generate_image",
"generate_tryon_tool",
"send_outfits",
"send_image_message",
]
[channels.wechat_kefu]
tools = []
[channels.wechat_miniapp]
tools = ["edit_outfit"]
[metadata]
category = "output"
owner = "main-agent"
[instructions]
zh = "skills/outfit_output_skill.zh.txt"
en = "skills/outfit_output_skill.en.txt"
Sources: data/skills/example/skill.toml, data/skills/outfit-output-skill/skill.toml
技能定义字段说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | 是 | 技能唯一标识符 |
name | string | 否 | 技能显示名称,默认使用 id |
description | string | 否 | 技能描述,用于 on-demand 技能的提示词生成 |
enable | bool | 否 | 是否启用,默认为 true |
activation | string | 否 | 激活模式:on_demand(按需)或 always(常驻),默认为 on_demand |
tools | list[string] | 否 | 全渠道共用的工具名称列表 |
allowed_tools | list[string] | 否 | 声明该技能推荐使用的工具边界(传递给 agno Skill) |
channels | table | 否 | 渠道专属工具配置,键为渠道名,值为包含 tools 列表的表 |
compatibility | string | 否 | 兼容性声明 |
license | string | 否 | 许可证信息 |
metadata | table | 否 | 技能元数据 |
instructions | table | 否 | 提示词文件路径映射,键为语言代码,值为相对于 data/prompts/ 的路径 |
Sources: app/skills/loader.py, data/skills/buyer-wardrobe-skill/skill.toml
技能加载流程
技能加载由 SkillRegistry 类负责,它在应用启动时扫描 data/skills/ 目录,解析所有技能定义文件并构建 RegisteredSkill 对象。
flowchart TD
A[应用启动] --> B[init_skill_registry]
B --> C[SkillRegistry.load_all]
C --> D[扫描 data/skills/ 目录]
D --> E{发现技能定义文件}
E -->|*.toml| F[解析单文件技能]
E -->|*/skill.toml| G[解析目录式技能]
F --> H[_build_skill]
G --> H
H --> I{检查 enable 字段}
I -->|false| J[跳过]
I -->|true| K[读取 TOML 配置]
K --> L[解析工具列表]
L --> M[解析渠道工具配置]
M --> N[解析提示词配置]
N --> O[发现 references/scripts 资源]
O --> P[从 TOOL_REGISTRY 解析工具对象]
P --> Q[创建 RegisteredSkill 对象]
Q --> R[存入 _skills 字典]
SkillRegistry 支持两种加载模式:
- 1. 基础加载(
user_id=None):加载默认配置,结果缓存在_skills字典 - 2. 灰度加载(
user_id指定):根据用户 ID 应用灰度覆盖,结果缓存在_variant_skills字典
Sources: app/skills/loader.py, app/skills/loader.py
RegisteredSkill 数据结构
RegisteredSkill 是技能加载后的核心数据结构,使用 @dataclass(frozen=True) 定义为不可变对象:
@dataclass(frozen=True)
class RegisteredSkill:
id: str # 技能 ID
name: str # 技能名称
description: str # 技能描述
activation: str # 激活模式: "on_demand" | "always"
definition_path: Path # 定义文件路径
source_path: Path # 技能源目录路径
enabled: bool # 是否启用
tool_names: list[str] # 全渠道工具名列表
channel_tools: dict[str, list[str]] # 渠道专属工具映射
tools: list[Any] # 已解析的工具对象列表
references: list[str] # 参考资料文件名列表
scripts: list[str] # 脚本文件名列表
allowed_tools: list[str] | None # 声明的工具边界
metadata: dict[str, Any] | None # 元数据
compatibility: str | None # 兼容性声明
license: str | None # 许可证
instructions_prompt: str | None # 提示词文件路径
skill: Skill # agno Skill 实例
raw_cfg: dict[str, Any] # 原始 TOML 配置
RegisteredSkill 提供了以下关键方法用于工具查询:
- -
all_tool_names():返回所有工具名(全渠道 + 各渠道专属工具的并集) - -
tools_for_channel(channel):返回指定渠道可用的工具名列表 - -
allowed_channels_for_tool(tool_name):返回某工具被限制使用的渠道列表 - -
is_tool_enabled_for_channel(tool_name, channel):判断工具在指定渠道是否可用
Sources: app/skills/loader.py
技能包(SkillBundle)构建
当主 Agent 需要使用技能时,通过 SkillRegistry.build_bundle() 方法将多个技能打包为 SkillBundle 对象。SkillBundle 聚合了所有选中技能的工具、提示词和访问工具。
flowchart LR
A[team_cfg.skills 列表] --> B[SkillRegistry.build_bundle]
C[SkillRegistry._skills] --> B
B --> D[遍历 skill_names]
D --> E[查找 RegisteredSkill]
E --> F[收集工具对象]
E --> G[收集 always 技能的提示词]
E --> H[收集 always 技能的内联指令]
F --> I[合并 access_tools]
I --> J[生成 on-demand 技能提示片段]
J --> K[返回 SkillBundle]
SkillBundle 包含以下关键属性:
| 属性 | 类型 | 说明 |
|---|---|---|
names | list[str] | 选中的技能 ID 列表 |
tools | list[Any] | 所有技能的工具对象列表 |
access_tools | list[Any] | 技能访问工具(get_skill_instructions 等) |
prompt_snippet | str | on-demand 技能的系统提示片段 |
always_instruction_prompts | list[str] | always 技能的提示词路径列表 |
inline_instructions | str | always 技能的内联指令文本 |
definitions | list[RegisteredSkill] | 选中的技能定义列表 |
team_tools 属性返回 tools 和 access_tools 的合并列表,用于注入主 Agent。
Sources: app/skills/loader.py
技能激活模式
技能支持两种激活模式,通过 activation 字段配置:
on_demand(按需激活)
按需技能不会自动注入系统提示词,而是通过生成提示片段告知 Agent 有哪些可用技能。Agent 在运行时通过调用 get_skill_instructions(skill_name="...") 工具动态加载技能指令。
提示片段格式如下:
<skills_system>
Available on-demand skills are listed below.
Before using a skill's worldview or business tools, call get_skill_instructions(skill_name="...").
Loading a skill activates it in dialog state for the current workflow step.
- nuva-soul: Nuva 的前台人格与护短表达层。
- style-advisor-skill: 负责时尚穿搭方案推荐、评论、审美解释和方案比较。
- buyer-wardrobe-skill: 负责搜单品、找同款、购物链接、衣橱管理。
- outfit-output-skill: 负责最终视觉输出,包括生成穿搭、试穿和发送最终结果。
</skills_system>
当前系统中的四个业务技能均采用 on_demand 模式:
| 技能 ID | 职责 | 关联工具 |
|---|---|---|
nuva-soul | 人格表达层 | 无专属工具 |
style-advisor-skill | 穿搭构想与审美解释 | 无专属工具 |
buyer-wardrobe-skill | 搜品、衣橱管理 | search_items, send_items 等 15 个工具 |
outfit-output-skill | 视觉输出 | generate_outfit, send_outfits 等 5 个工具 |
Sources: app/skills/loader.py, data/team/main.toml
always(常驻激活)
常驻技能的指令提示词会自动注入主 Agent 的系统提示词中,无需 Agent 主动调用。always_instruction_prompts 列表中的提示词路径会被传递给 build_main_team_instructions(),在每次构建指令时自动加载。
Sources: app/skills/loader.py, app/agents/main_team_prompt.py
技能运行时访问工具
技能系统提供了三个运行时访问工具,使 Agent 能够在对话过程中动态加载技能内容:
# app/skills/access_tools.py
async def get_skill_instructions(run_context, skill_name: str) -> str
async def get_skill_reference(run_context, skill_name: str, reference_name: str) -> str
async def get_skill_script(run_context, skill_name: str, script_name: str) -> str
| 工具名 | 功能 | 副作用 |
|---|---|---|
get_skill_instructions | 加载技能的指令提示词 | 调用 activate_skill() 激活技能 |
get_skill_reference | 读取技能的参考资料文件 | 调用 activate_skill() 激活技能 |
get_skill_script | 读取技能的脚本文件 | 调用 activate_skill() 激活技能 |
当 Agent 调用 get_skill_instructions 时,系统会:
- 1. 从
SkillRegistry查找指定技能 - 2. 调用
activate_skill(session_state, skill.id)将技能写入对话状态 - 3. 加载并返回技能的指令提示词
activate_skill() 函数会更新对话状态中的 active_skill 和 loaded_skills 字段,这直接影响工具门控机制。
Sources: app/skills/access_tools.py, app/dialog_state.py
工具门控机制
技能系统与工具门控机制紧密配合,确保技能拥有的工具只有在技能被激活后才能被调用。这是防止 Agent 在错误的业务阶段调用不相关工具的关键安全机制。
sequenceDiagram
participant Agent as 主 Agent
participant Factory as 工具工厂
participant DialogState as 对话状态
participant SkillRegistry as 技能注册表
Agent->>Factory: 调用工具 (如 search_items)
Factory->>SkillRegistry: get_tool_owning_skills("search_items")
SkillRegistry-->>Factory: ["buyer-wardrobe-skill"]
Factory->>DialogState: get_dialog_state(session_state)
DialogState-->>Factory: {active_skill: "style-advisor-skill"}
Factory->>Factory: is_tool_allowed() 检查
Note over Factory: active_skill ≠ owning_skill
Factory-->>Agent: 返回 tool_not_available 错误
工具门控的判断逻辑在 is_tool_allowed() 函数中实现:
def is_tool_allowed(session_state, *, tool_name, owning_skills) -> bool:
if tool_name in ALWAYS_ALLOWED_TOOLS: # 常驻白名单工具
return True
owners = [item for item in (owning_skills or []) if item]
if not owners: # 无技能拥有此工具
return True
active_skill = get_dialog_state(session_state).get("active_skill")
return bool(active_skill) and active_skill in owners
常驻白名单工具(不受门控限制):
- -
run_outfit_flow、update_user_profile、new_session - -
send_text_message、send_miniprogram_message - -
get_skill_instructions、get_skill_reference、get_skill_script - -
get_dialog_state、update_dialog_state
Sources: app/dialog_state.py, app/dialog_state.py, app/tools/factory.py
渠道工具适配
技能支持为不同渠道配置专属工具,通过 skill.toml 中的 [channels.<channel_name>] 段落定义。运行时,tools_for_channel() 方法会合并全渠道工具和指定渠道的专属工具。
以 outfit-output-skill 为例:
- - 全渠道工具:
generate_outfit,generate_image,generate_tryon_tool,send_outfits,send_image_message - - 微信客服渠道:无额外工具(
tools = []) - - 小程序渠道:额外增加
edit_outfit
渠道工具适配在两个层面生效:
- 1. 技能层面:
RegisteredSkill.is_tool_enabled_for_channel()检查工具是否在当前渠道可用 - 2. 主 Agent 层面:
MainTeamConfig.is_tool_enabled_for_channel()检查主 Agent 级别的渠道限制
Sources: data/skills/outfit-output-skill/skill.toml, app/skills/loader.py
灰度发布支持
技能配置支持按用户 ID 进行灰度覆盖。SkillRegistry 使用灰度配置服务(GrayConfigService)对技能定义文件应用 TOML 级别的覆盖。
灰度流程:
- 1. 计算灰度签名:遍历所有技能定义路径,构建
skill:<path>格式的 scope 列表 - 2. 查询灰度规则:根据用户 ID 和 scope 列表查询匹配的灰度规则
- 3. 合并配置:使用
get_toml_overlay()将灰度规则中的 TOML 片段合并到基础配置 - 4. 缓存变体:灰度后的技能映射按签名缓存在
_variant_skills字典
# 灰度覆盖示例:为用户 42 增加工具
# GrayRuleRecord:
# namespace="skill"
# path="skills/buyer-wardrobe-skill/skill.toml"
# users_expr="42"
# content='tools = ["search_items", "send_items"]'
灰度覆盖会同时影响 build_bundle() 和 get_tool_owning_skills() 的结果,确保灰度用户的工具门控逻辑与配置一致。
Sources: app/skills/loader.py, tests/test_skill_support.py
主 Agent 集成
技能系统在主 Agent 构建时(_build_agent())完成集成。集成过程如下:
flowchart TD
A[_build_agent] --> B[加载 team_cfg]
B --> C[获取技能注册表]
C --> D[build_bundle: 打包技能]
D --> E[获取主 Agent 基础工具]
E --> F[合并工具列表]
F --> G[构建运行时工具解析器]
G --> H[构建指令解析器]
H --> I[注入提示片段到 additional_context]
I --> J[创建 Agent 实例]
关键集成点:
- 1. 工具合并:主 Agent 的基础工具(来自
team_cfg.tools)与技能工具(来自SkillBundle.team_tools)合并 - 2. 指令注入:
always技能的提示词自动注入系统指令;on_demand技能的提示片段注入additional_context - 3. 运行时工具解析:通过
_resolve_runtime_tools函数,每次运行时根据用户 ID 动态解析工具描述
Sources: app/agent.py, app/agent.py
热重载机制
技能系统支持运行时热重载,通过 reload_agents() 函数触发:
def reload_agents() -> None:
init_tool_registry() # 重建工具注册表
init_skill_registry() # 重建技能注册表
get_main_team_loader().reload() # 重载团队配置
_agent_variants.clear() # 清除 Agent 缓存
_agent = _build_agent(leader_runtime=leader_runtime) # 重建 Agent
热重载会清除所有缓存(包括灰度变体缓存),重新扫描 data/skills/ 目录并重建整个 Agent。这使得在不重启服务的情况下,修改技能定义文件或提示词文件即可生效。
Sources: app/agent.py, app/skills/loader.py
技能提示词管理
技能的指令提示词存放在 data/prompts/skills/ 目录下,通过 skill.toml 中的 [instructions] 段落引用。提示词文件支持中英文双语,运行时根据用户语言偏好动态选择。
当前系统中的技能提示词文件:
| 技能 | 中文提示词 | 英文提示词 |
|---|---|---|
| nuva-soul | skills/nuva_soul.zh.txt | skills/nuva_soul.en.txt |
| style-advisor-skill | skills/style_advisor_skill.zh.txt | skills/style_advisor_skill.en.txt |
| buyer-wardrobe-skill | skills/buyer_wardrobe_skill.zh.txt | skills/buyer_wardrobe_skill.en.txt |
| outfit-output-skill | skills/outfit_output_skill.zh.txt | skills/outfit_output_skill.en.txt |
提示词内容定义了技能的职责边界、执行规则、输出格式等。Agent 在加载技能指令后,必须严格按照提示词中的约束执行。
Sources: app/skills/loader.py, data/prompts/skills/
技能资源文件
技能可以携带参考资料(references)和脚本(scripts)两类资源文件,分别存放在技能源目录的 references/ 和 scripts/ 子目录中。资源文件在技能加载时自动发现,并通过运行时访问工具提供给 Agent。
- - 参考资料:通过
get_skill_reference(skill_name, reference_name)读取,适用于分类法、指南等静态知识 - - 脚本文件:通过
get_skill_script(skill_name, script_name)读取,适用于辅助计算或数据处理逻辑
Sources: app/skills/loader.py, app/skills/access_tools.py
下一步
- - 了解技能工具门控的详细实现机制,请参阅 技能工具门控机制
- - 了解工具注册与工厂模式,请参阅 工具注册与工厂
- - 了解对话状态如何影响技能激活,请参阅 Dialog State 设计
- - 了解如何新增技能模块,请参阅 新增技能模块
<a id="8-ji-neng-gong-ju-men-kong-ji-zhi"></a>
---
技能工具门控机制
- - Section: 架构与核心概念
- - Group: Agent 与技能系统
- - Source File:
8-ji-neng-gong-ju-men-kong-ji-zhi.md
本文档详细阐述 Outfit Agent 中技能(Skill)与工具(Tool)之间的访问控制机制——即"门控"(Gating)。门控机制确保工具只在正确的技能上下文和正确的渠道中可被调用,防止 Agent 跨技能边界误用工具,同时支持按渠道精细控制工具可见性。
门控架构总览
Outfit Agent 采用单主 Agent 架构,所有技能的工具在运行时被注入同一个 Agent。门控机制在工具调用链路中提供两层运行时校验:技能归属门控和渠道可用性门控。当 Agent 调用任意业务工具时,工具包装层(_wrap_tool_entrypoint)会依次执行这两层检查,只有全部通过后工具才能真正执行。
flowchart TD
A["Agent 调用工具"] --> B["工具包装层\n(_wrap_tool_entrypoint)"]
B --> C{"渠道门控检查\n(_tool_available_for_current_channel)"}
C -->|"团队级渠道限制不通过"| D["返回 tool_not_available_on_channel 错误"]
C -->|"技能级渠道限制不通过"| D
C -->|"通过"| E{"技能归属门控检查\n(is_tool_allowed)"}
E -->|"工具在 ALWAYS_ALLOWED_TOOLS 中"| F["工具正常执行"]
E -->|"无技能归属(公共工具)"| F
E -->|"当前 active_skill 匹配工具归属技能"| F
E -->|"不匹配"| G["返回 tool_not_available 错误"]
F --> H["执行工具入口函数"]
style D fill:#f8d7da,stroke:#dc3545,color:#721c24
style G fill:#f8d7da,stroke:#dc3545,color:#721c24
style F fill:#d4edda,stroke:#28a745,color:#155724
style H fill:#d4edda,stroke:#28a745,color:#155724
Sources: factory.py, dialog_state.py
技能归属门控
技能归属门控是门控机制的第一层(在渠道门控之后执行)。其核心思想是:工具必须归属于当前处于激活状态的技能,才能被调用。
归属关系建立
每个技能在 skill.toml 中通过 tools 字段声明它拥有的工具列表。SkillRegistry 加载时会扫描所有技能配置,并建立工具名到技能 ID 的反向映射。
# data/skills/buyer-wardrobe-skill/skill.toml
id = "buyer-wardrobe-skill"
tools = [
"search_items",
"search_items_batch",
"search_wardrobe",
"get_item_detail",
"save_item_batch",
# ... 更多工具
]
get_tool_owning_skills 方法遍历所有已注册技能,返回包含指定工具的技能 ID 列表。例如查询 "search_items" 的归属时,返回 ["buyer-wardrobe-skill"]。
Sources: loader.py, buyer-wardrobe-skill/skill.toml
门控判定逻辑
is_tool_allowed 函数实现了三层短路判定:
| 判定条件 | 结果 | 说明 |
|---|---|---|
工具名在 ALWAYS_ALLOWED_TOOLS 中 | ✅ 允许 | 核心工具不受门控限制 |
owning_skills 为空列表 | ✅ 允许 | 工具不属于任何技能,视为公共工具 |
active_skill 存在且在 owning_skills 中 | ✅ 允许 | 当前激活的技能拥有此工具 |
| 其他情况 | ❌ 拒绝 | 工具属于某个技能,但当前未激活该技能 |
ALWAYS_ALLOWED_TOOLS 是一个固定的白名单集合,包含以下工具:run_outfit_flow、update_user_profile、new_session、send_text_message、send_miniprogram_message、get_skill_instructions、get_skill_reference、get_skill_script、get_dialog_state、update_dialog_state。这些工具是系统运转的基础设施,不应被门控阻断。
Sources: dialog_state.py, dialog_state.py
技能激活流程
技能处于被动激活状态——Agent 需要主动调用访问工具(Access Tools)来激活技能。access_tools.py 提供了三个访问工具:
- 1.
get_skill_instructions(skill_name):读取技能的提示词指令,并将该技能标记为当前会话的active_skill - 2.
get_skill_reference(skill_name, reference_name):读取技能附属的参考文档,同时激活技能 - 3.
get_skill_script(skill_name, script_name):读取技能附属的脚本文件,同时激活技能
调用任一访问工具都会触发 activate_skill,将技能 ID 写入 dialog_state.active_skill,同时追加到 dialog_state.loaded_skills 列表。这意味着同一时刻只有一个技能处于激活状态——后激活的技能会替换先前的激活状态。
Agent 在对话过程中会收到一段由 _build_prompt_snippet 生成的系统提示,其中列出所有可用的 on_demand 技能,并引导 Agent 在使用技能前先调用 get_skill_instructions。
Sources: access_tools.py, dialog_state.py, loader.py
错误响应格式
当技能归属门控拒绝工具调用时,返回标准化的 JSON 错误:
{
"error": "tool_not_available",
"tool_name": "search_items",
"required_skill": ["buyer-wardrobe-skill"],
"message": "Tool 'search_items' requires activating one of: buyer-wardrobe-skill"
}
Agent 收到此错误后,应先调用 get_skill_instructions(skill_name="buyer-wardrobe-skill") 激活对应技能,再重新调用该工具。
Sources: dialog_state.py
渠道可用性门控
渠道门控是门控机制的第二层(在技能归属门控之前执行),负责确保工具只在指定的渠道中可用。渠道门控有两个层级:团队级和技能级。
团队级渠道限制
在 data/team/main.toml 中,通过 [channels.<channel_name>] 段为不同渠道声明专属工具。运行时,MainTeamConfig 的 allowed_channels_for_tool 方法会返回工具被允许使用的渠道列表。
# data/team/main.toml
tools = ["run_outfit_flow", "update_user_profile", "new_session", ...]
[channels.wechat_kefu]
tools = ["send_miniprogram_message"]
如果 send_miniprogram_message 被声明在 [channels.wechat_kefu] 下,则该工具只能在 wechat_kefu 渠道中使用,在 wechat_miniapp 渠道中调用将返回错误。
Sources: main.toml, team_loader.py
技能级渠道限制
每个技能也可以在 skill.toml 中通过 [channels.<channel_name>] 为特定渠道声明额外的或受限的工具。例如 outfit-output-skill 配置:
# data/skills/outfit-output-skill/skill.toml
tools = ["generate_outfit", "generate_image", "generate_tryon_tool", "send_outfits", "send_image_message"]
[channels.wechat_kefu]
tools = []
[channels.wechat_miniapp]
tools = ["edit_outfit"]
此配置表明 edit_outfit 是该技能的渠道专属工具,仅在 wechat_miniapp 渠道中可用。当 active_skill 为 outfit-output-skill 且当前渠道为 wechat_kefu 时,调用 edit_outfit 会被技能级渠道门控拦截。
RegisteredSkill 通过以下方法管理渠道工具:
- -
all_tool_names():返回基础工具 + 所有渠道工具的合并列表 - -
tools_for_channel(channel):返回指定渠道下该技能可用的工具名列表 - -
allowed_channels_for_tool(tool_name):返回允许使用该工具的渠道列表(空列表表示全渠道可用) - -
is_tool_enabled_for_channel(tool_name, channel):判定工具在指定渠道是否可用
Sources: outfit-output-skill/skill.toml, loader.py
渠道门控执行流程
_tool_available_for_current_channel 函数按以下顺序检查:
- 1. 从当前会话的
session_state中解析出current_channel - 2. 团队级检查:查询团队配置的
allowed_channels_for_tool,若工具被限制且当前渠道不在允许列表中,直接返回错误 - 3. 技能级检查:如果存在
active_skill,查询该技能的allowed_channels_for_tool,若工具被限制且当前渠道不在允许列表中,返回错误 - 4. 两级检查均通过,返回
None(允许通过)
渠道门控错误的 JSON 格式:
{
"error": "tool_not_available_on_channel",
"tool_name": "edit_outfit",
"allowed_channels": ["wechat_miniapp"],
"message": "Tool 'edit_outfit' is only available for channels: wechat_miniapp"
}
Sources: factory.py, factory.py
工具包装与注入
门控机制通过 build_tool 和 _wrap_tool_entrypoint 统一注入到每个工具中。这是一个透明的包装层,对工具实现代码完全无侵入。
build_tool 包装流程
build_tool 函数接收一个原始的工具入口函数,执行以下步骤:
- 1. 调用
_wrap_tool_entrypoint包装入口函数,注入渠道门控和技能归属门控逻辑 - 2. 通过
Function.from_callable创建 agnoFunction对象 - 3. 从
tools/descriptions.toml加载工具描述 - 4. 处理
__tool_stop_after_tool_call__、__tool_stop_after_tool_call_if__、__tool_show_result__等装饰属性
包装后的工具在每次调用时会:
- 1. 绑定 Agno 上下文(用于链路追踪)
- 2. 执行渠道门控检查
- 3. 执行技能归属门控检查
- 4. 两层检查均通过后,调用原始入口函数
- 5. 记录追踪信息
Sources: factory.py, factory.py
工具注入时机
在 _build_agent 函数中,工具通过两个来源合并注入到主 Agent:
- 1. 团队工具:
get_main_agent_tools(team_cfg)根据团队配置解析基础工具列表 - 2. 技能工具:
team_skill_bundle.team_tools包含所有技能声明的工具和访问工具
合并后,工具列表通过 materialize_tools_for_user 为当前用户加载个性化的工具描述(支持灰度覆盖),最终注入 Agent。
Sources: agent.py
完整门控示例
以下通过一个端到端示例展示门控机制的完整运作:
sequenceDiagram
participant U as 用户
participant A as Agent
participant GW as 工具包装层
participant DS as DialogState
U->>A: "帮我搜一下白色连衣裙"
Note over A: Agent 决策:需要使用 search_items 工具
A->>GW: 调用 search_items(...)
GW->>DS: 查询 active_skill
DS-->>GW: active_skill = null(未激活任何技能)
GW-->>A: 返回 tool_not_available 错误
Note over A: Agent 意识到需要先激活技能
A->>GW: 调用 get_skill_instructions("buyer-wardrobe-skill")
GW->>DS: 检查 get_skill_instructions 是否在 ALWAYS_ALLOWED_TOOLS
Note over GW: ✅ 在白名单中,允许执行
GW->>DS: activate_skill("buyer-wardrobe-skill")
DS-->>GW: active_skill = "buyer-wardrobe-skill"
GW-->>A: 返回技能指令文本
A->>GW: 调用 search_items(query="白色连衣裙")
GW->>DS: 查询 active_skill
DS-->>GW: active_skill = "buyer-wardrobe-skill"
GW->>GW: search_items 归属于 buyer-wardrobe-skill ✅
GW-->>A: 执行成功,返回搜索结果
A-->>U: "找到以下白色连衣裙..."
Sources: test_skill_tool_gating.py
灰度配置支持
门控机制完整支持灰度配置覆盖。SkillRegistry 和 MainTeamLoader 都通过 GrayConfigService 为特定用户加载覆盖版本的技能配置。
灰度路径为 skills/<skill-id>/skill.toml,覆盖后可以改变技能的 tools 列表、channels 配置等。这意味着运营人员可以针对特定用户开启或关闭某些工具、调整渠道限制,而无需修改代码或重启服务。
SkillRegistry 通过 _gray_signature 方法为每个用户构建唯一的签名,缓存该用户对应的技能变体,避免重复加载。invalidate 方法用于在灰度配置变更时清除缓存。
Sources: loader.py, loader.py, test_skill_support.py
关键设计决策
| 设计选择 | 理由 |
|---|---|
同一时刻仅一个 active_skill | 简化门控逻辑,避免多技能同时激活导致的工具冲突 |
| 访问工具作为激活入口 | 技能按需加载,Agent 先获取指令再使用工具,符合认知流程 |
ALWAYS_ALLOWED_TOOLS 白名单 | 基础设施工具(对话状态、消息发送等)在任何阶段都应可用 |
| 双层渠道门控 | 团队级提供全局渠道策略,技能级提供细粒度的渠道适配 |
| 工具包装层透明注入 | 工具实现无需关心门控逻辑,降低代码耦合 |
| 灰度配置覆盖 | 支持 A/B 测试和渐进式功能发布 |
Sources: dialog_state.py, factory.py, loader.py
参考与导航
- - 前置阅读:技能加载与管理 了解技能的定义、目录结构与加载流程
- - 前置阅读:单主 Agent 架构 了解技能工具注入的上下文
- - 后续阅读:工具注册与工厂 了解工具的注册机制与 build_tool 详情
- - 后续阅读:工具描述管理 了解工具描述的加载与灰度覆盖
- - 后续阅读:Dialog State 设计 了解 active_skill 等状态字段的完整设计
<a id="9-dialog-state-she-ji"></a>
---
Dialog State 设计
- - Section: 架构与核心概念
- - Group: 对话状态与 SOP 流程
- - Source File:
9-dialog-state-she-ji.md
Dialog State 是 Outfit Agent 的会话级有限状态机,它将一次对话交互的上下文——从技能激活、信息收集、SOP 阶段推进到穿搭工作流生命周期——统一编码为一个嵌套字典,存储于 Agno Session 的 session_state["dialog_state"] 键下。该设计的核心目标是:让 Agent 在每一轮对话中都能"知道自己在哪里",同时为工具门控、提示词注入和后台可观测性提供统一的状态基底。
整体架构与数据流
Dialog State 在系统中的位置可以用以下数据流来概括:Agent 工具调用(读/写)→ session_state["dialog_state"](内存态)→ Agno Session 持久化 → 后台异步写入(persist_dialog_state_update)。读取路径覆盖提示词注入、工具门控和 Trace 属性构建三条并行分支。
flowchart LR
subgraph Agent Runtime
A[Agent LLM] -->|工具调用| B[dialog_state_tools]
B -->|读写| C[session_state<br/>dialog_state]
end
subgraph Prompt Injection
C -->|describe_dialog_state_for_prompt| D[profile_hook<br/>additional_context]
end
subgraph Tool Gating
C -->|is_tool_allowed| E[factory.py<br/>build_tool wrapper]
end
subgraph Observability
C -->|_dialog_state_summary| F[live_runs.py]
C -->|_build_trace_attributes| G[Trace Attributes]
end
subgraph Async Persistence
H[followup_intent<br/>workflow_callback] -->|persist_dialog_state_update| I[dialog_state_store]
I -->|asave_session| J[(Agno Session DB)]
end
Sources: app/dialog_state.py, app/services/dialog_state_store.py, app/tools/dialog_state_tools.py
状态字典结构
DEFAULT_DIALOG_STATE 定义了 Dialog State 的完整骨架。每个字段承担明确的职责,不存在语义重叠。以下是字段分类与类型说明:
| 字段 | 类型 | 职责分类 | 说明 | |
|---|---|---|---|---|
version | int | 元数据 | 当前固定为 1,预留 schema 演进能力 | |
active_skill | `str \ | None` | 技能上下文 | 当前激活的技能 ID,影响工具门控 |
loaded_skills | list[str] | 技能上下文 | 已加载的技能 ID 列表(去重、保序) | |
active_scene | `str \ | None` | 业务场景 | 当前业务场景标识(如 outfit_recommendation) |
stage | `str \ | None` | 业务场景 | 泛化业务阶段标记 |
sop_stage | `str \ | None` | SOP 推进 | SOP 流水线阶段,详见下方 SOP 阶段表 |
collection_plan | `dict \ | None` | 信息收集 | 信息收集策略(task_kind、required_slots、missing_slots、optional_slots) |
collected_slots | dict | 信息收集 | 已收集的槽值(键值对) | |
idea_summary | `dict \ | None` | 信息收集 | 构想摘要(如风格、核心单品) |
outfit_task | `dict \ | None` | 工作流生命周期 | 穿搭任务清单,传递给下游 workflow |
outfit_status | `str \ | None` | 工作流生命周期 | 工作流运行状态 |
outfit_run_id | `str \ | None` | 工作流生命周期 | 后台 workflow 的唯一运行标识 |
outfit_result_summary | `dict \ | None` | 工作流生命周期 | 工作流结果摘要 |
pending_confirmation | `dict \ | None` | 用户确认 | 待确认事项(含 action 和 summary) |
updated_at | int | 元数据 | 最后更新的 Unix 时间戳 |
Sources: app/dialog_state.py
核心操作函数
状态读取与初始化
系统提供两个读取函数,分别服务于可写场景和只读场景,选择不同的合并策略:
- -
ensure_dialog_state(session_state)— 可写路径。在现有状态与DEFAULT_DIALOG_STATE之间做浅合并(现有字段优先),然后直接写回session_state["dialog_state"]。适合工具执行过程中需要修改状态的场景。 - -
get_dialog_state(session_state)— 只读路径。同样做合并但不写回原始session_state,返回一个独立的字典副本。适合提示词注入、Trace 属性构建等不应产生副作用的场景。
两个函数都会调用 _normalize_skill_list() 对 loaded_skills 做去重保序处理,并确保 collected_slots 始终为 dict 类型。
Sources: app/dialog_state.py
状态更新
update_dialog_state_fields() 是状态写入的唯一核心函数,所有更新路径最终都汇聚于此。它接受丰富的 keyword-only 参数,支持增量更新和条件清除两种模式:
- - 增量更新:传入非
None的参数直接覆盖对应字段。collected_slots采用浅合并策略(新键覆盖旧键,旧键保留)。 - - 条件清除:通过
clear_*布尔参数显式清空对应字段(如clear_outfit_run_id=True将outfit_run_id设为None)。 - - 全局重置:
clear_other=True会将整个状态回退到DEFAULT_DIALOG_STATE,仅保留本次调用明确写入的新值。适用于用户切换到全新任务的场景。
值得注意的是,该函数内嵌了一条状态归一化逻辑:当 sop_stage 为 await_outfit_confirmation 且 pending_confirmation.action 为 run_outfit_flow 时,如果 outfit_status 为空或仍为 running,系统会自动将其归一化为 awaiting_confirmation,同时清空 outfit_run_id 和 outfit_result_summary。这条规则确保了"等待确认"状态的一致性。
Sources: app/dialog_state.py
技能激活
activate_skill() 是技能加载的专用入口,执行以下原子操作:
- 1. 调用
ensure_dialog_state()确保状态已初始化 - 2. 将
skill_name追加到loaded_skills(若不存在) - 3. 将
active_skill设为skill_name - 4. 可选地同时写入
sop_stage和active_scene
Sources: app/dialog_state.py
工具门控机制
Dialog State 驱动了系统的技能级工具门控,这是防止 Agent 在错误的 SOP 阶段调用不相关工具的核心安全机制。
门控规则
is_tool_allowed(session_state, tool_name, owning_skills) 的判定逻辑:
- 1. 若
tool_name在ALWAYS_ALLOWED_TOOLS集合中 → 始终放行 - 2. 若
owning_skills为空(工具不属于任何技能)→ 始终放行 - 3. 否则,检查
active_skill是否在owning_skills列表中 → 匹配则放行,否则拒绝
ALWAYS_ALLOWED_TOOLS 包含 10 个系统级工具:run_outfit_flow、update_user_profile、new_session、send_text_message、send_miniprogram_message、get_skill_instructions、get_skill_reference、get_skill_script、get_dialog_state、update_dialog_state。这些工具在任何 SOP 阶段都可被调用。
flowchart TD
A[Agent 调用工具] --> B{tool_name 在<br/>ALWAYS_ALLOWED_TOOLS?}
B -->|是| C[✅ 放行]
B -->|否| D{owning_skills 为空?}
D -->|是| C
D -->|否| E{active_skill 在<br/>owning_skills 中?}
E -->|是| C
E -->|否| F[❌ 拒绝<br/>返回 tool_not_available 错误]
工厂层封装
app/tools/factory.py 中的 build_tool() 将 is_tool_allowed() 的检查逻辑封装在每个工具的入口 wrapper 中。每次工具调用都会经历以下检查链:渠道门控 → 技能门控 → 实际执行。门控失败时返回结构化 JSON 错误,包含 error、tool_name、required_skill 和 message 字段,供 Agent 理解拒绝原因并调整策略。
Sources: app/dialog_state.py, app/tools/factory.py
SOP 阶段与工作流生命周期
sop_stage 字段是 SOP 流水线的阶段标识符,它驱动了从需求探索到视觉交付的完整主线漏斗。outfit_status 字段则独立追踪后台工作流的运行生命周期。两者协同工作但语义独立:
sop_stage 阶段表
| 阶段值 | 对应主线漏斗 | 说明 |
|---|---|---|
None | 游离状态 | 未进入主线漏斗 |
collecting_idea | [S1] 需求探索 | 信息收集进行中 |
await_idea_confirmation | [S1] 需求探索 | 构想摘要已写入,等待用户确认 |
searching_items | [S1] 需求探索 | 搜品进行中(buyer-wardrobe-skill) |
await_search_confirmation | [S1] 需求探索 | 搜品结果已展示,等待用户确认 |
await_outfit_confirmation | [S2] 提案确认 | 任务清单已就绪,等待用户"发车" |
outfit_running | [S2] 后台生成 | 工作流正在后台执行 |
s3_visual_delivered | [S3] 视觉交付 | 视觉方案已交付给用户 |
outfit_failed | [S2]/[S3] 异常 | 工作流执行失败 |
outfit_status 状态表
| 状态值 | 说明 |
|---|---|
None | 无活跃工作流 |
awaiting_confirmation | 等待用户确认启动 |
running | 工作流正在执行 |
completed | 工作流完成 |
partial_failed | 部分失败(如某套方案生成失败) |
failed | 完全失败 |
interrupted | 被用户新消息中断 |
状态转换流程
stateDiagram-v2
[*] --> collecting_idea: activate_skill / 信息收集
collecting_idea --> await_idea_confirmation: 构想确认
await_idea_confirmation --> searching_items: 搜品开始
searching_items --> await_search_confirmation: 搜品完成
await_search_confirmation --> await_outfit_confirmation: 任务单写入
await_outfit_confirmation --> outfit_running: run_outfit_flow
outfit_running --> s3_visual_delivered: workflow 完成
outfit_running --> outfit_failed: workflow 失败
outfit_running --> await_outfit_confirmation: 用户中断
s3_visual_delivered --> [*]
outfit_failed --> await_outfit_confirmation: 重新确认
outfit_failed --> collecting_idea: 回退到 S1
Sources: app/dialog_state.py, app/services/outfit_workflow_service.py, app/tools/outfit_workflow_tools.py, data/prompts/agents/main_team.zh.txt
待确认机制 (Pending Confirmation)
pending_confirmation 字典是 Dialog State 中的用户确认门控,用于在关键 SOP 阶段断点处强制等待用户的明确意图。其结构包含两个固定键:
- -
action:标识待确认的操作类型(如run_outfit_flow、search_items、confirm_search_results) - -
summary:面向 Agent 的确认提示文本
该机制在以下场景中被激活:
- 1. 穿搭任务发车确认:当
outfit_task写入后,pending_confirmation.action设为run_outfit_flow,sop_stage推进到await_outfit_confirmation。只有当用户明确表达"开始"等意图时,Agent 才被允许执行run_outfit_flow。 - 2. 搜品结果确认:
buyer-wardrobe-skill完成搜品后,通过pending_confirmation等待用户对结果的认可。 - 3. 工作流中断恢复:当用户在工作流运行期间发送新消息导致中断时,系统通过
persist_dialog_state_update()将状态回退到await_outfit_confirmation,并写入中断相关的pending_confirmation。
Sources: app/dialog_state.py, app/tools/outfit_workflow_tools.py, app/wechat_followup_intent.py
提示词注入
Dialog State 不仅是后台状态管理的工具,还通过 describe_dialog_state_for_prompt() 函数直接注入到 Agent 的系统提示词中。该函数被 app/agents/profile_hook.py 调用,在每次 Agent 运行前将有意义的状态字段序列化为 JSON 文本,拼接到 agent.additional_context 中。
注入的字段包括:active_skill、loaded_skills、active_scene、stage、sop_stage、collection_plan、collected_slots、idea_summary、outfit_task、outfit_status、outfit_run_id、outfit_result_summary、pending_confirmation。若所有字段均为空/None,该函数返回 None 以避免污染提示词。
这种设计使得 Agent 在每一轮对话开始时就完全了解当前的 SOP 进度和待处理事项,无需额外的工具调用来"回忆"状态。
Sources: app/dialog_state.py, app/agents/profile_hook.py
异步持久化
当 Dialog State 需要在 Agent 运行上下文之外被修改时(如微信 follow-up intent 仲裁、workflow 回调),app/services/dialog_state_store.py 提供了 persist_dialog_state_update() 函数。该函数的执行路径为:
- 1. 通过
aread_or_create_session()从 Agno Session Store 加载会话 - 2. 通过
load_session_state()恢复session_state - 3. 调用
update_dialog_state_fields()应用增量更新 - 4. 将更新后的状态写回
session_data["session_state"] - 5. 通过
asave_session()持久化
该函数接受 session_id、user_id 和 update_kwargs(直接透传给 update_dialog_state_fields()),确保了 Agent 运行时和异步回调对 Dialog State 的操作使用同一套合并逻辑。
Sources: app/services/dialog_state_store.py
可观测性
Dialog State 的关键字段被系统性地暴露到两个可观测性通道:
- 1. 实时运行快照(
app/live_runs.py):_dialog_state_summary()提取 11 个核心字段的摘要,用于 Dashboard 实时展示会话状态。 - 2. Trace 属性(
app/tools/factory.py):_build_trace_attributes()将active_skill、active_scene、sop_stage、outfit_status、outfit_run_id、pending_confirmation和loaded_skills作为 Span 属性附加到每次工具调用的 Trace 中,支持链路追踪中的状态关联查询。
Admin 端的 Trace 查询接口(_build_session_summary)也会从 session_data 中提取 Dialog State 字段,为管理面板提供完整的会话上下文视图。
Sources: app/live_runs.py, app/tools/factory.py, tests/test_admin_traces_dialog_state.py
Agent 工具接口
Agent 通过两个工具函数与 Dialog State 交互,这两个函数注册在 STATE_TOOLS 分组中,并列入 ALWAYS_ALLOWED_TOOLS:
| 工具函数 | 方向 | 参数 |
|---|---|---|
get_dialog_state | 读 | 无(从 run_context.session_state 自动获取) |
update_dialog_state | 写 | active_scene、stage、sop_stage、_json(JSON 字符串参数)、outfit_status、outfit_run_id、clear_、clear_other |
update_dialog_state 工具函数是 update_dialog_state_fields() 的薄封装,负责将 LLM 传入的 JSON 字符串参数解析为字典,对 outfit_task 执行 normalize_outfit_task_payload_data() 标准化,以及将 pending_confirmation_action + pending_confirmation_summary 组装为 pending_confirmation 字典。
工具描述定义在 data/tools/descriptions.toml 中,支持按用户维度的灰度覆盖。
<a id="10-sop-jie-duan-tui-jin"></a>
---
SOP 阶段推进
- - Section: 架构与核心概念
- - Group: 对话状态与 SOP 流程
- - Source File:
10-sop-jie-duan-tui-jin.md
SOP 阶段推进是 Outfit Agent 的对话流程控制核心,它通过 dialog_state.sop_stage 字段将一次完整的穿搭交互——从需求探索、信息收集、任务确认到后台工作流执行——编码为一条可观测、可回退、可中断的有限状态管线。每个阶段不仅决定了 Agent 的合法战术动作,还驱动了工具门控、用户确认断点和异步工作流生命周期管理。
主线漏斗与 SOP 阶段映射
系统将一次穿搭交互划分为四个主线漏斗阶段(S1–S4),每个阶段在主 Agent 提示词中定义了明确的战术动作,同时映射到 sop_stage 的具体枚举值。以下是完整的阶段对照:
| 漏斗阶段 | sop_stage 值 | 语义说明 | 核心动作 |
|---|---|---|---|
| 游离状态 | None | 未进入主线漏斗 | 寒暄、闲聊、异常拦截 |
| S1 需求探索 | collecting_idea | 信息收集进行中 | 意图识别、槽位补全、气象查询 |
| S1 需求探索 | await_idea_confirmation | 构想摘要已产出,等待用户确认方向 | 发送方案概要,等待反馈 |
| S1 需求探索 | searching_items | 搜品进行中(buyer-wardrobe-skill) | 调用搜品工具,筛选候选 |
| S1 需求探索 | await_search_confirmation | 搜品结果已展示,等待用户确认 | 展示候选,等待采纳或调整 |
| S2 提案确认 | await_outfit_confirmation | 任务清单已就绪,等待用户"发车" | 展示任务清单,等待"开始"指令 |
| S2 后台生成 | outfit_running | 工作流正在后台执行 | 抑制前端交互,安抚等待 |
| S3 视觉交付 | s3_visual_delivered | 视觉方案已交付给用户 | 发送穿搭图与总结,进入微调/闭环 |
| 异常 | outfit_failed | 工作流执行失败 | 发送失败提示,引导重试 |
Sources: app/dialog_state.py, data/prompts/agents/main_team.zh.txt, app/services/outfit_workflow_service.py
状态转换流程
SOP 阶段遵循一条主线正向推进路径,同时在工作流中断和失败场景下支持回退。以下状态图展示了完整的转换关系:
stateDiagram-v2
[*] --> collecting_idea: activate_skill / 信息收集
collecting_idea --> await_idea_confirmation: 构想摘要产出
await_idea_confirmation --> searching_items: 用户确认方向
searching_items --> await_search_confirmation: 搜品完成
await_search_confirmation --> await_outfit_confirmation: 任务清单写入
await_outfit_confirmation --> outfit_running: run_outfit_flow
outfit_running --> s3_visual_delivered: workflow 完成
outfit_running --> outfit_failed: workflow 失败
outfit_running --> await_outfit_confirmation: 用户中断 / followup
s3_visual_delivered --> [*]
outfit_failed --> await_outfit_confirmation: 重新确认
outfit_failed --> collecting_idea: 回退到 S1
Sources: app/services/outfit_workflow_service.py, app/tools/outfit_workflow_tools.py, app/wechat_followup_intent.py
SOP 阶段写入路径
sop_stage 的写入通过 update_dialog_state_fields() 统一汇聚,但触发入口分布在三个层面:Agent 工具调用、工作流服务、以及异步事件回调。
Agent 工具调用路径
主 Agent 通过 update_dialog_state 工具直接推进 SOP 阶段。该工具接受 sop_stage 参数,内部调用 update_dialog_state_fields() 完成写入。主提示词中明确规定了 S1→S2 的关键转换动作:当 collection_plan.missing_slots 已清空时,Agent 调用 update_dialog_state(sop_stage="await_outfit_confirmation", pending_confirmation_action="run_outfit_flow") 将控制权交给用户确认断点。
Sources: app/tools/dialog_state_tools.py, data/prompts/agents/main_team.zh.txt
工作流工具路径
run_outfit_flow 是 await_outfit_confirmation → outfit_running 的唯一合法入口。该工具内置了严格的前置检查——只有当 sop_stage == "await_outfit_confirmation" 且 pending_confirmation.action == "run_outfit_flow" 时才放行。执行成功后,通过 resolve_outfit_workflow_sop_stage() 将工作流状态映射为最终 SOP 阶段:
- -
completed或partial_failed→s3_visual_delivered - - 其他状态 →
outfit_failed
def resolve_outfit_workflow_sop_stage(status: str | None) -> str:
normalized_status = str(status or "").strip()
if normalized_status in _DELIVERED_WORKFLOW_STATUSES:
return "s3_visual_delivered"
return "outfit_failed"
Sources: app/tools/outfit_workflow_tools.py, app/tools/outfit_workflow_tools.py, app/services/outfit_workflow_service.py
异步事件路径
当用户在 outfit_running 阶段发送新消息导致工作流中断时,系统通过两条路径回退 SOP 阶段:
- 1.
wechat_followup_intent.py:当 followup 意图仲裁器判定为interrupt动作且检测到穿搭工作流正在运行时,通过persist_dialog_state_update()将sop_stage回退到await_outfit_confirmation,同时清除outfit_run_id并写入中断相关的pending_confirmation。 - 2.
outfit_workflow_service.py:工作流内部捕获asyncio.CancelledError时,同样回退到await_outfit_confirmation。服务启动时的recover_interrupted_workflows()方法还会扫描数据库中所有outfit_status == "running"的会话,根据工作流实际运行状态决定恢复为s3_visual_delivered或await_outfit_confirmation。
Sources: app/wechat_followup_intent.py, app/services/outfit_workflow_service.py, app/services/outfit_workflow_service.py
用户确认断点机制
SOP 管线中的关键阶段转换都经过 pending_confirmation 字段进行门控。该字段包含 action(待确认的操作类型)和 summary(面向 Agent 的确认提示文本)两个固定键。当 sop_stage 为 await_outfit_confirmation 且 pending_confirmation.action 为 run_outfit_flow 时,只有当用户明确表达"开始"等意图后,Agent 才被允许执行 run_outfit_flow。
系统在 update_dialog_state_fields() 中内嵌了一条状态归一化逻辑:当上述两个条件同时满足时,如果 outfit_status 为空或仍为 running,系统会自动将其归一化为 awaiting_confirmation,同时清空 outfit_run_id 和 outfit_result_summary。这条规则确保了"等待确认"状态的一致性,防止残留的工作流运行态干扰后续判断。
Sources: app/dialog_state.py, app/tools/outfit_workflow_tools.py
SOP 阶段与工具门控的协同
sop_stage 与工具门控体系协同工作,共同约束 Agent 在每个阶段的合法行为。factory.py 在构建工具上下文时将 sop_stage 注入到运行时快照中(键名为 sop.stage),供下游工具和提示词注入使用。技能级工具门控由 active_skill 字段驱动,而 SOP 阶段则通过 describe_dialog_state_for_prompt() 直接注入到 Agent 系统提示词中,使 Agent 在每一轮对话开始时就完全了解当前的 SOP 进度和待处理事项,无需额外的工具调用来"回忆"状态。
flowchart LR
subgraph 每轮对话开始
A[Agent 启动] --> B[profile_hook]
B -->|describe_dialog_state_for_prompt| C[系统提示词注入 sop_stage]
end
subgraph 工具调用时
D[Agent LLM] -->|调用工具| E[factory.py]
E -->|is_tool_allowed| F{active_skill 门控}
E -->|sop.stage 注入| G[工具上下文]
end
subgraph 关键断点
H[sop_stage == await_outfit_confirmation] --> I{pending_confirmation 检查}
I -->|通过| J[run_outfit_flow 放行]
I -->|未通过| K[返回错误]
end
Sources: app/tools/factory.py, app/dialog_state.py, app/agents/profile_hook.py
工作流生命周期与 SOP 阶段的对应关系
outfit_status 字段独立追踪后台工作流的运行生命周期,与 sop_stage 协同但语义独立。两者之间的对应关系如下:
| sop_stage | outfit_status | 含义 |
|---|---|---|
await_outfit_confirmation | awaiting_confirmation | 任务清单就绪,等待用户发车 |
outfit_running | running | 工作流正在后台执行 |
s3_visual_delivered | completed / partial_failed | 视觉方案已交付(全部/部分成功) |
outfit_failed | failed | 工作流执行失败 |
await_outfit_confirmation(中断恢复) | interrupted | 被用户新消息中断,回退到确认态 |
工作流本身由 OutfitGenerationWorkflow 定义,包含五个顺序 Step:prepare_context → ideate_outfits → select_items → generate_images(并发)→ dispatch_results。dispatch_results 根据成功/失败数量决定最终状态:全部成功 → completed,部分成功 → partial_failed,全部失败 → failed。只有全部成功时才发送总结文案,否则只发送成功部分和失败提示。
Sources: app/workflows/outfit_generation.py, app/services/outfit_workflow_service.py, data/i18n/workflow.zh.json
工作流中断与恢复策略
系统在三个层面处理工作流中断:
前端 followup 中断:当 outfit_running 阶段用户发送新消息时,followup 意图仲裁器根据消息类型(情绪催促、局部微调、意图颠覆、无关闲聊)决定是否中断。中断时通过 persist_dialog_state_update() 回退状态并取消当前 Agno Run。
工作流内部中断:run_outfit_flow 工具在捕获 asyncio.CancelledError 时,将 sop_stage 回退到 await_outfit_confirmation,并将 outfit_result_summary 标记为 interrupted,同时写入 pending_confirmation 以支持用户后续恢复。
服务重启恢复:OutfitWorkflowService.recover_interrupted_workflows() 在服务启动时扫描数据库中所有 outfit_status == "running" 的会话,查询对应工作流的最新运行状态,然后做出恢复决策:如果工作流已完成则更新为最终状态,如果工作流已失败则回退到 await_outfit_confirmation 并提示用户恢复。
Sources: app/wechat_followup_intent.py, app/services/outfit_workflow_service.py, app/services/outfit_workflow_service.py
防打断策略
主提示词在 S2 阶段定义了四类用户消息的防打断策略,这些策略通过 Agent 的提示词层执行,与 SOP 阶段共同维护工作流的原子性:
| 用户意图类型 | 示例 | 策略 | 是否中断管线 |
|---|---|---|---|
| 情绪催促 | "好期待"、"怎么这么慢" | 安抚等待,维持悬念 | 否 |
| 局部参数微调 | "能换成红色的吗" | 强势压制,拒绝热更新 | 否 |
| 意图颠覆 | "算了,去滑雪吧" | 硬核熔断,切回 S1 | 是 |
| 无关闲聊 | "时装周有什么新鲜事" | 简短回应,拉回注意力 | 否 |
只有意图颠覆(底层场景或核心意图发生根本性反转)才会触发真正的管线中断,其他三类都通过话术引导用户耐心等待工作流完成。
<a id="11-yong-hu-que-ren-ji-zhi"></a>
---
用户确认机制
- - Section: 架构与核心概念
- - Group: 对话状态与 SOP 流程
- - Source File:
11-yong-hu-que-ren-ji-zhi.md
用户确认机制是 NUVA Fashion OS 中确保穿搭生成工作流可控执行的核心安全机制。该机制通过对话状态管理(Dialog State)和 SOP 阶段控制,在关键操作执行前强制插入用户确认环节,防止未经用户授权的后台任务执行,同时支持工作流中断后的状态恢复。
机制概述与设计原则
用户确认机制的核心设计遵循"显式授权"原则:任何涉及资源密集型操作(如穿搭图生成、单品搜索、试穿渲染)的任务,都必须在用户明确确认后才能执行。这种设计既避免了系统资源的浪费,也确保了用户对生成结果的预期管理。
该机制通过三个核心状态字段实现:
- -
sop_stage:SOP 流程阶段标识,标记当前对话在穿搭工作流中的位置 - -
pending_confirmation:待确认事项,包含需要用户确认的操作类型和描述 - -
outfit_status:穿搭工作流状态,标记后台任务的执行状态
当系统完成需求收集并准备好执行生成任务时,会进入"等待确认"状态;只有用户明确表达"开始生成"等意图后,系统才会执行实际的生成操作。
Sources: dialog_state.py, outfit_workflow_tools.py
核心数据结构与状态流转
用户确认机制的核心数据结构定义在 dialog_state.py 中,通过 DEFAULT_DIALOG_STATE 初始化:
DEFAULT_DIALOG_STATE: dict[str, Any] = {
"version": 1,
"active_skill": None,
"loaded_skills": [],
"active_scene": None,
"stage": None,
"sop_stage": None,
"collection_plan": None,
"collected_slots": {},
"idea_summary": None,
"outfit_task": None,
"outfit_status": None,
"outfit_run_id": None,
"outfit_result_summary": None,
"pending_confirmation": None,
"updated_at": 0,
}
其中 pending_confirmation 字段是一个字典,包含两个关键属性:
- -
action:需要确认的操作类型(如"run_outfit_flow") - -
summary:向用户展示的确认描述文本
状态流转遵循严格的时序逻辑:
- 1. 需求收集阶段:系统通过
collection_plan收集用户需求,sop_stage为collecting_requirements - 2. 任务准备阶段:需求收集完成后,系统写入
outfit_task,sop_stage变为await_outfit_confirmation - 3. 用户确认阶段:
pending_confirmation.action设置为run_outfit_flow,等待用户确认 - 4. 执行阶段:用户确认后,
sop_stage变为outfit_running,outfit_status变为running - 5. 完成阶段:工作流完成后,
sop_stage变为s3_visual_delivered
Sources: dialog_state.py, dialog_state.py
工具层实现与门控逻辑
用户确认机制在工具层通过 outfit_workflow_tools.py 和 outfit_tools.py 实现严格的门控逻辑。核心函数 _is_outfit_confirmation_ready 负责检查当前状态是否允许执行穿搭工作流:
def _is_outfit_confirmation_ready(
dialog_state: dict[str, Any],
*,
allowed_actions: set[str],
) -> bool:
pending_confirmation = dialog_state.get("pending_confirmation") or {}
return (
dialog_state.get("sop_stage") == "await_outfit_confirmation"
and pending_confirmation.get("action") in allowed_actions
)
run_outfit_flow 工具在执行前会进行双重验证:
- 1. 状态验证:检查
sop_stage是否为await_outfit_confirmation - 2. 确认验证:检查
pending_confirmation.action是否为run_outfit_flow
如果验证失败,工具会返回错误信息,阻止工作流执行。同时,outfit_tools.py 中的 generate_outfit 工具也会检查是否存在待确认的经典工作流,防止绕过确认机制:
if (
str(collection_plan.get("task_kind") or "").strip() == "classic_outfit"
and pending_confirmation.get("action") == "run_outfit_flow"
):
return _build_classic_workflow_bypass_error()
这种双重门控机制确保了即使用户尝试通过其他工具绕过确认,系统也能正确拦截。
Sources: outfit_workflow_tools.py, outfit_workflow_tools.py, outfit_tools.py
工作流中断与状态恢复
用户确认机制的一个重要特性是支持工作流中断后的状态恢复。当用户在穿搭工作流执行期间发送新消息时,系统会通过 wechat_followup_intent.py 中的中断处理逻辑:
- 1. 检测中断条件:通过
_should_reset_outfit_confirmation_state函数检测是否需要中断 - 2. 更新对话状态:将
sop_stage重置为await_outfit_confirmation - 3. 设置待确认事项:创建新的
pending_confirmation,提示用户是否恢复中断的工作流 - 4. 取消当前任务:调用
agent.acancel_run取消正在执行的 agno run
中断后的状态恢复通过 _build_interrupted_update 函数实现:
def _build_interrupted_update(
*,
lang: str,
interrupted_run_id: str | None,
) -> dict[str, object]:
return {
"sop_stage": "await_outfit_confirmation",
"outfit_status": "awaiting_confirmation",
"clear_outfit_run_id": True,
"outfit_result_summary": {
"status": "interrupted",
"error": "workflow_interrupted_by_followup",
"interrupted_run_id": interrupted_run_id,
},
"pending_confirmation": {
"action": "run_outfit_flow",
"summary": get_text(
"workflow",
"followup_interrupt_pending_confirmation",
lang=lang,
),
},
}
这种设计确保了即使工作流被中断,用户仍然可以选择恢复执行,而不需要重新开始整个流程。
Sources: wechat_followup_intent.py, outfit_workflow_service.py
工具描述与用户交互
用户确认机制在工具描述中明确告知用户触发条件和预期行为。run_outfit_flow 工具的描述强调:
只有当dialog_state.sop_stage == await_outfit_confirmation且pending_confirmation.action == run_outfit_flow时才能执行;否则会返回错误。
update_dialog_state 工具的描述也明确了如何设置待确认事项:
若需要用户确认,可传入pending_confirmation_action与pending_confirmation_summary;若要清空待确认事项,可传入clear_pending_confirmation=true。
在用户交互层面,系统会通过提示词指导 Agent 正确使用确认机制。例如,在 main_team.zh.txt 中明确要求:
只有当历史状态已显示sop_stage=await_outfit_confirmation,且用户明确表达"开始/开始生成/就按这个开始/生成吧"等含义时,才允许直接执行run_outfit_flow。
Sources: descriptions.toml, descriptions.toml, main_team.zh.txt
测试用例与边界情况
用户确认机制通过全面的测试用例验证其正确性。测试覆盖以下关键场景:
- 1. 状态验证测试:验证
run_outfit_flow在未满足确认条件时返回错误 - 2. 正常执行测试:验证工作流在正确确认后能够正常执行
- 3. 状态更新测试:验证工作流完成后对话状态正确更新
- 4. 中断恢复测试:验证工作流被中断后的状态恢复逻辑
例如,test_run_outfit_flow_requires_confirmation_state 测试验证了当 sop_stage 不是 await_outfit_confirmation 时,工具会返回 outfit_flow_not_ready_to_run 错误。
边界情况测试包括:
- - 工作流执行期间用户发送新消息
- - 用户在确认阶段补充信息而非确认开始
- - 工作流失败后的重试逻辑
- - 多个工作流实例的并发控制
这些测试确保了用户确认机制在各种边界情况下都能正确工作,防止系统进入不一致状态。
Sources: test_outfit_workflow_tools.py, test_dialog_state.py
与 SOP 阶段的集成
用户确认机制与 SOP 阶段紧密集成,形成完整的穿搭工作流控制链。SOP 阶段通过 sop_stage 字段标记当前流程位置:
| SOP 阶段 | 描述 | 与确认机制的关系 |
|---|---|---|
collecting_requirements | 需求收集阶段 | 无确认需求,系统主动收集信息 |
await_outfit_confirmation | 等待用户确认 | 核心确认阶段,需要用户明确确认 |
outfit_running | 工作流执行中 | 确认已完成,系统执行生成任务 |
s3_visual_delivered | 视觉交付完成 | 工作流完成,进入反馈阶段 |
outfit_failed | 工作流失败 | 可能需要重新确认或修正任务 |
在样式顾问技能(style_advisor_skill.zh.txt)中,明确要求在需求足够时立即设置确认状态:
需求足够时,立即调用update_dialog_state写入outfit_task,并把outfit_status="awaiting_confirmation"、sop_stage="await_outfit_confirmation"、pending_confirmation.action="run_outfit_flow"一起写回。
这种集成确保了用户确认机制在 SOP 流程中的正确位置,既不会过早触发,也不会遗漏必要的确认步骤。
Sources: outfit_workflow_service.py, style_advisor_skill.zh.txt
最佳实践与开发指南
在使用用户确认机制时,开发者应遵循以下最佳实践:
1. 正确设置确认状态
# 正确示例:在需求收集完成后设置确认状态
update_dialog_state_fields(
session_state,
sop_stage="await_outfit_confirmation",
outfit_task=task_payload,
outfit_status="awaiting_confirmation",
pending_confirmation={
"action": "run_outfit_flow",
"summary": "是否开始生成两套穿搭图",
},
)
2. 验证确认状态
在执行敏感操作前,始终验证确认状态:
if not _is_outfit_confirmation_ready(
dialog_state,
allowed_actions={"run_outfit_flow"},
):
return json.dumps({"error": "outfit_flow_not_ready_to_run"})
3. 处理中断情况
在工作流执行期间,正确处理用户中断:
try:
result = await service.run_prepared_workflow(request=request, tool_ctx=ctx)
except asyncio.CancelledError:
# 设置中断状态,等待用户确认恢复
update_dialog_state_fields(
session_state,
sop_stage="await_outfit_confirmation",
pending_confirmation={
"action": "run_outfit_flow",
"summary": "是否恢复中断的工作流",
},
)
raise
4. 清理确认状态
工作流完成后,清理确认状态:
update_dialog_state_fields(
session_state,
clear_pending_confirmation=True,
)
5. 避免绕过确认
不要尝试通过其他工具绕过确认机制,系统会检测并阻止此类行为。
Sources: dialog_state.py, outfit_workflow_tools.py
架构图示
stateDiagram-v2
[*] --> collecting_requirements: 开始对话
collecting_requirements --> await_outfit_confirmation: 需求收集完成
await_outfit_confirmation --> outfit_running: 用户确认开始
await_outfit_confirmation --> collecting_requirements: 用户补充信息
outfit_running --> s3_visual_delivered: 工作流完成
outfit_running --> outfit_failed: 工作流失败
outfit_running --> await_outfit_confirmation: 用户中断
s3_visual_delivered --> [*]: 进入反馈阶段
outfit_failed --> await_outfit_confirmation: 重新确认
outfit_failed --> collecting_requirements: 重新收集需求
state await_outfit_confirmation {
[*] --> 等待用户确认
等待用户确认 --> 确认开始: 用户说"开始生成"
等待用户确认 --> 补充信息: 用户补充需求
}
相关页面
- - Dialog State 设计:了解对话状态管理的完整设计
- - SOP 阶段推进:了解 SOP 阶段的完整流程
- - 工具注册与工厂:了解工具的注册和门控机制
- - 会话分发与回复状态:了解会话管理和中断处理
<a id="12-gong-ju-zhu-ce-yu-gong-han"></a>
---
工具注册与工厂
- - Section: 架构与核心概念
- - Group: 工具系统
- - Source File:
12-gong-ju-zhu-ce-yu-gong-han.md
本文档详细阐述 outfit_agent 系统中工具(Tool)的注册机制、工厂模式实现以及相关的安全门控体系。工具系统是连接 LLM 智能体与业务逻辑的核心桥梁,通过统一的注册表(Registry)和工厂模式(Factory Pattern)实现工具的标准化创建、动态加载和运行时访问控制。
核心架构:三层注册体系
outfit_agent 的工具系统采用三层注册架构,从底层到上层分别为:原始函数层、包装工具层、注册表层。这种分层设计确保了业务逻辑与 LLM 交互的安全隔离。
graph TB
subgraph "原始函数层"
A["业务函数<br/>(outfit_biz)"]
B["工具包装函数<br/>(app/tools/*.py)"]
end
subgraph "包装工具层"
C["build_tool()<br/>工厂函数"]
D["Function 对象<br/>(agno.tools.Function)"]
end
subgraph "注册表层"
E["TOOL_REGISTRY<br/>全局工具注册表"]
F["SkillBundle<br/>技能工具包"]
end
A -->|"user_id, lang 等敏感参数<br/>由 ToolContext 注入"| B
B -->|"纯业务意图参数<br/>(搜索词、ID 等)"| C
C -->|"包装、追踪、门控"| D
D -->|"按名称注册"| E
E -->|"按技能声明分组"| F
style A fill:#e1f5fe
style B fill:#f3e5f5
style C fill:#fff3e0
style D fill:#e8f5e8
style E fill:#fce4ec
style F fill:#f1f8e9
Sources: app/tools/__init__.py, app/tools/factory.py
工具工厂模式详解
工具工厂(Tool Factory)是整个工具系统的核心,通过 build_tool() 函数实现从原始业务函数到 agno Function 对象的转换。
工厂函数签名
def build_tool(entrypoint: Callable[..., Any], *, name: str | None = None) -> Function
Sources: app/tools/factory.py
工厂处理流程
build_tool() 执行以下关键步骤:
- 1. 名称解析:优先使用显式
name参数,否则从函数名推导 - 2. 入口包装:通过
_wrap_tool_entrypoint()为函数添加安全上下文注入和追踪能力 - 3. 描述加载:从
data/tools/descriptions.toml加载工具描述,支持用户级灰度覆盖 - 4. 行为配置:根据函数装饰器属性设置
stop_after_tool_call、show_result等行为 - 5. 钩子安装:为动态停止条件安装
pre_hook和post_hook
flowchart TD
A["build_tool(entrypoint)"] --> B["解析工具名称"]
B --> C["_wrap_tool_entrypoint()"]
C --> D["Function.from_callable()"]
D --> E["加载工具描述"]
E --> F{"检查装饰器属性"}
F -->|"__tool_stop_after_tool_call__"| G["设置 stop_after_tool_call"]
F -->|"__tool_stop_after_tool_call_if__"| H["安装动态停止钩子"]
F -->|"__tool_show_result__"| I["设置 show_result"]
G --> J["返回 Function 对象"]
H --> J
I --> J
style A fill:#fff3e0
style J fill:#e8f5e8
Sources: app/tools/factory.py, app/tools/factory.py
工具描述管理
工具描述采用双层缓存机制,支持基础描述和用户级灰度覆盖:
| 缓存层级 | 缓存函数 | 最大容量 | 说明 |
|---|---|---|---|
| 基础层 | _load_base_tool_descriptions() | 1 | 从 data/tools/descriptions.toml 加载全局描述 |
| 用户层 | _load_tool_descriptions(user_id) | 128 | 基础层 + 用户级灰度覆盖 |
Sources: app/tools/factory.py, data/tools/descriptions.toml
安全包装机制
RunContext 与 ToolContext
工具包装的核心安全目标是防止 LLM 越权访问用户数据。通过 RunContext 注入会话级上下文,确保 LLM 无法直接操作 user_id、lang 等敏感参数。
classDiagram
class RunContext {
+str run_id
+str session_id
+str user_id
+dict session_state
+dict dependencies
+dict metadata
}
class ToolContext {
+int user_id
+str user_id_str
+str lang
+str session_id
+str avatar_path
+str avatar_info
+str channel
+dict channel_data
+replace(**changes)
+to_session_value()
}
class _wrap_tool_entrypoint {
+_async_wrapper(*args, **kwargs)
+_sync_wrapper(*args, **kwargs)
}
RunContext --> "session_state[tool_ctx]" ToolContext : 包含
_wrap_tool_entrypoint --> RunContext : 读取
_wrap_tool_entrypoint --> ToolContext : 注入业务函数
Sources: app/tools/context.py, app/tools/factory.py
包装函数的双重保障
_wrap_tool_entrypoint() 为每个工具函数添加以下安全机制:
- 1. 上下文绑定:通过
bind_agno_context()绑定session_id、run_id、user_id到追踪上下文 - 2. 调用追踪:通过
trace_tool_call()记录工具调用的输入、输出和属性 - 3. 渠道门控:检查工具是否在当前渠道(channel)可用
- 4. 技能门控:检查工具是否在当前激活的技能(skill)中声明
Sources: app/tools/factory.py, app/tools/factory.py
工具注册表(TOOL_REGISTRY)
注册表结构
TOOL_REGISTRY 是一个全局字典,映射工具名称到 Function 对象。它在应用启动时通过 init_tool_registry() 初始化。
TOOL_REGISTRY: dict[str, Function] = {}
Sources: app/tools/__init__.py
注册表构建流程
_build_tool_registry() 函数从各个工具模块收集原始函数,通过 build_tool() 转换为 Function 对象:
| 工具模块 | 工具函数示例 | 说明 |
|---|---|---|
canvas_tools | search_canvas_items, save_outfit_from | 画布相关工具 |
item_tools | search_items, get_item_detail | 单品管理工具 |
outfit_tools | generate_outfit, edit_outfit | 穿搭管理工具 |
image_gen_tools | generate_image, generate_tryon_tool | 图片生成工具 |
shopping_tools | get_item_links_tool, get_cheapest_link_tool | 购物链接工具 |
user_tools | update_user_profile, new_session | 用户管理工具 |
messaging_tools | send_text_message, send_outfits | 消息发送工具 |
weather | query_weather_now, query_weather_forecast | 天气查询工具 |
Sources: app/tools/__init__.py
工具分组常量
除了全局注册表,系统还维护多个工具分组常量,便于按业务域批量引用:
| 分组常量 | 包含工具 | 用途 |
|---|---|---|
ALL_BIZ_TOOLS | 除天气外的所有业务工具 | 主 Agent 默认工具集 |
ITEM_TOOLS | 单品相关工具 | 衣橱管理 |
OUTFIT_TOOLS | 穿搭相关工具 | 穿搭生成与编辑 |
IMAGE_GEN_TOOLS | 图片生成工具 | 图片生成与试穿 |
SHOPPING_TOOLS | 购物链接工具 | 购买渠道管理 |
MESSAGING_TOOLS | 消息发送工具 | 消息推送 |
STATE_TOOLS | 对话状态工具 | 状态管理 |
CANVAS_TOOLS | 画布工具 | 画布操作 |
Sources: app/tools/__init__.py
动态工具加载机制
基于 Team 配置的加载
主 Agent 的工具集通过 data/team/main.toml 配置,支持全渠道基础工具和渠道专属工具:
# 全渠道基础工具
tools = [
"run_outfit_flow",
"update_user_profile",
"new_session",
"get_dialog_state",
"update_dialog_state",
"query_weather_now",
"query_weather_forecast",
"query_weather_hourly",
]
# 渠道专属工具
[channels.wechat_kefu]
tools = ["send_miniprogram_message"]
Sources: data/team/main.toml, app/agents/team_loader.py
工具解析与验证
resolve_tools_by_names() 函数从注册表中按名称解析工具,并验证工具存在性:
def resolve_tools_by_names(tool_names: Iterable[str], *, owner_id: str) -> list[Function]
Sources: app/tools/__init__.py
工具门控机制(Tool Gating)
门控架构
工具门控是运行时访问控制的核心,确保工具只能在正确的技能上下文中被调用。
flowchart TD
A["工具调用请求"] --> B{"工具在 ALWAYS_ALLOWED_TOOLS 中?"}
B -->|"是"| C["允许调用"]
B -->|"否"| D{"工具有声明所属技能?"}
D -->|"否"| C
D -->|"是"| E{"当前激活技能匹配?"}
E -->|"是"| C
E -->|"否"| F["返回门控错误"]
style C fill:#e8f5e8
style F fill:#ffebee
Sources: app/dialog_state.py, app/dialog_state.py
常驻允许工具
以下工具不受门控限制,始终可用:
- -
run_outfit_flow- 穿搭工作流 - -
update_user_profile- 用户档案更新 - -
new_session- 新会话 - -
send_text_message- 文本消息 - -
send_miniprogram_message- 小程序消息 - -
get_skill_instructions- 技能指令获取 - -
get_skill_reference- 技能参考获取 - -
get_skill_script- 技能脚本获取 - -
get_dialog_state- 对话状态获取 - -
update_dialog_state- 对话状态更新
Sources: app/dialog_state.py
渠道门控
除了技能门控,系统还支持渠道门控,限制工具在特定渠道(如微信客服、小程序)中可用:
def _tool_available_for_current_channel(run_context: Any, tool_name: str) -> str | None
Sources: app/tools/factory.py
动态停止条件
条件停止钩子
某些工具(如 run_outfit_flow)需要根据返回结果动态决定是否停止 Agent 运行。通过 _install_dynamic_stop_after_hook() 实现:
def _install_dynamic_stop_after_hook(
tool: Function,
*,
condition: Callable[[Any], bool],
) -> None
工作原理:
- 1.
pre_hook:每次调用前重置stop_after_tool_call = False - 2.
post_hook:调用后根据condition(result)动态设置stop_after_tool_call
Sources: app/tools/factory.py, tests/test_tool_factory.py
技能工具绑定
技能声明工具
每个技能在 skill.toml 中声明自己拥有的工具:
[skill]
tool_names = ["search_items", "get_item_detail", ...]
Sources: app/skills/loader.py
技能工具包(SkillBundle)
当技能被激活时,系统构建 SkillBundle,包含技能声明的工具和访问工具:
@dataclass(frozen=True)
class SkillBundle:
names: list[str]
tools: list[Any]
access_tools: list[Any]
prompt_snippet: str
always_instruction_prompts: list[str]
inline_instructions: str
skills: object | None
definitions: list[RegisteredSkill]
Sources: app/skills/loader.py
工具调用追踪
追踪属性构建
每次工具调用都会记录以下追踪属性:
def _build_trace_attributes(run_context: Any) -> dict[str, Any]:
return {
"skill.id": dialog_state.get("active_skill"),
"scene.code": dialog_state.get("active_scene"),
"sop.stage": dialog_state.get("sop_stage"),
"workflow.status": dialog_state.get("outfit_status"),
"workflow.run_id": dialog_state.get("outfit_run_id"),
"dialog.awaiting_confirmation": bool(pending_confirmation),
"dialog.loaded_skills": dialog_state.get("loaded_skills") or [],
}
Sources: app/tools/factory.py
最佳实践
新增工具的步骤
- 1. 创建工具函数:在
app/tools/下对应模块中创建业务函数 - 2. 添加包装函数:在函数签名中添加
run_context: RunContext参数,通过_get_ctx()获取上下文 - 3. 注册到 TOOLS 列表:在模块末尾的
TOOLS列表中添加函数引用 - 4. 更新注册表:在
app/tools/__init__.py的_build_tool_registry()中添加映射 - 5. 添加工具描述:在
data/tools/descriptions.toml中添加工具描述 - 6. 配置技能声明:在相应技能的
skill.toml中声明工具
安全注意事项
- - 永远不要在工具函数签名中暴露
user_id、lang等敏感参数 - - 始终使用
_get_ctx(run_context)获取用户上下文 - - 验证权限:在访问用户数据前检查
ctx.user_id是否匹配 - - 处理缺失上下文:当
_get_ctx()返回None时返回_NO_CTX_RESULT
Sources: app/tools/context.py, app/tools/item_tools.py
相关页面
- - 工具描述管理 - 了解工具描述的加载、缓存和灰度机制
- - 工具调用链路追踪 - 了解工具调用的追踪和监控机制
- - 技能工具门控机制 - 了解技能与工具的绑定和访问控制
- - Dialog State 设计 - 了解对话状态管理,这是工具门控的基础
- - 新增业务工具 - 了解如何扩展新的业务工具
<a id="13-gong-ju-miao-shu-guan-li"></a>
---
工具描述管理
- - Section: 架构与核心概念
- - Group: 工具系统
- - Source File:
13-gong-ju-miao-shu-guan-li.md
工具描述管理是 Outfit Agent 系统中用于控制 LLM 工具行为的核心机制。通过集中化的描述文件和灵活的加载策略,系统能够精确指导 LLM 如何使用每个工具,同时支持灰色发布以进行 A/B 测试和用户级别的定制化调整。
工具描述文件结构与格式
所有工具描述集中存储在 data/tools/descriptions.toml 文件中,采用 TOML 格式。每个工具在 [tools] 表下有一个键值对,其中键为工具名称,值为该工具的多行描述字符串。
文件结构示例:
[tools]
generate_image = '''
适合用户想修改已有穿搭图或上一张 tryon 试穿图时调用。
根据文字提示词直接生成 1 张图片,可选附带 0~9 张参考图,输出比例固定为 3:4。
必须提供 `prompt`;可选传 `ref_images`,其中每一项都应是系统里已有的 `image_id`,总数最多 9 张。
本工具成功后会自动发送生成图片;
返回 JSON 对象,通常包含生成结果的 `image_id`、`oss_url` 和发送结果。
'''
search_items = '''
在公共商品库中按文本搜索单品。
适合当你已经有明确的文字检索条件,需要为用户找新商品、补齐穿搭缺失单品,或按款式/颜色/材质/场景查找候选单品时调用。
如果同一轮里要同时搜索多件不同单品,优先使用 `search_items_batch`,把所有请求一次性提交,减少重复 embedding 和多次串行搜品。
传入的 `query` 建议优先整理成英文关键词,便于和单品 VLM 英文字段、BM25 检索保持一致;
可选传入 `product_type`、`sub_category`、`leaf_category` 缩小品类,传入 `image_id` 做图文联合检索,并用 `limit` 控制结果数。
若传分类参数,必须使用精确 taxonomy 值;系统会优先尝试三级分类过滤,命中数不足时自动回退到二级/一级分类。
如果用户已经给了参考图,且目标是“找图中单品 / 找同款 / 找相似款”,不要把本工具当默认入口;那种场景应优先使用 `search_items_by_image_tool`。
这里的 `image_id` 更适合“文字检索为主、图片仅辅助排序”的混合检索,不适合作为参考图找同款的主流程。
不要在仅需搜索用户自有衣橱时调用本工具;那种场景应使用 `search_wardrobe`。
对于"搜同款发给我/给我看看候选"这类场景,拿到多个候选后通常应继续调用 `send_items` 做视觉展示,而不是只返回长段文字。
返回 JSON 数组,每项通常包含 `item_id`、`product_type`、`sub_category`、`leaf_category`、`brand`、`description`、`image_id`、`price` 等字段。
'''
描述编写规范:
- 1. 场景说明:明确描述何时使用该工具,避免误用
- 2. 参数说明:详细说明必选和可选参数,包括格式要求
- 3. 行为边界:明确工具的使用限制和替代方案
- 4. 返回格式:说明返回的数据结构和字段含义
- 5. 注意事项:包含重要的警告和最佳实践
Sources: data/tools/descriptions.toml
工具描述加载机制
系统采用分层缓存策略加载工具描述,确保性能的同时支持灰色发布。
加载流程:
- 1. 基础描述加载:从
data/tools/descriptions.toml文件读取基础描述 - 2. 用户覆盖加载:检查是否有针对当前用户的灰色发布描述
- 3. 描述合并:将用户特定描述与基础描述合并
- 4. 缓存管理:使用 LRU 缓存避免重复文件读取
关键函数:
@lru_cache(maxsize=1)
def _load_base_tool_descriptions() -> dict[str, str]:
"""加载基础工具描述"""
content = get_file("tools/descriptions.toml")
data = tomllib.loads(content)
return dict(data["tools"])
@lru_cache(maxsize=128)
def _load_tool_descriptions(user_id: str | None = None) -> dict[str, str]:
"""加载工具描述,支持用户级覆盖"""
if user_id is None:
return _load_base_tool_descriptions()
# 加载用户特定描述
content = get_file("tools/descriptions.toml", user_id=user_id)
data = tomllib.loads(content)
overlay = data["tools"]
# 合并描述
return {
**_load_base_tool_descriptions(),
**{str(key): str(value) for key, value in overlay.items()},
}
缓存策略:
- - 基础描述:全局缓存,应用生命周期内只加载一次
- - 用户描述:LRU 缓存,最多缓存 128 个用户描述
- - 缓存清除:通过
clear_tool_description_cache()函数手动清除
Sources: app/tools/factory.py
工具描述应用流程
工具描述在工具构建和用户会话初始化时应用到工具对象。
工具构建阶段:
def build_tool(entrypoint: Callable[..., Any], *, name: str | None = None) -> Function:
"""构建工具对象并应用描述"""
tool_name = name or entrypoint.__name__
wrapped = _wrap_tool_entrypoint(entrypoint, tool_name=tool_name)
tool = Function.from_callable(wrapped, name=tool_name)
# 应用工具描述
tool.description = get_tool_description(tool_name)
# 处理其他属性
stop_after_tool_call = bool(getattr(entrypoint, "__tool_stop_after_tool_call__", False))
# ...
return tool
用户会话阶段:
def materialize_tools_for_user(
tools: Sequence[Function | Callable[..., Any]],
*,
user_id: int | str | None = None,
) -> list[Function | Callable[..., Any]]:
"""为特定用户物化工具描述"""
resolved_user_id = _normalize_tool_description_user_id(user_id)
descriptions = _load_tool_descriptions(resolved_user_id)
materialized: list[Function | Callable[..., Any]] = []
for tool in tools:
if not isinstance(tool, Function):
materialized.append(tool)
continue
# 深拷贝工具对象避免影响原始工具
copied = tool.model_copy(deep=True)
description = descriptions.get(copied.name)
# 应用用户特定描述
if isinstance(description, str) and description.strip():
copied.description = description
materialized.append(copied)
return materialized
工具注册流程:
# 1. 在 app/tools/__init__.py 中初始化工具注册表
TOOL_REGISTRY: dict[str, Function] = {}
def _build_tool_registry() -> dict:
"""构建工具注册表"""
raw_registry = {
"search_items": search_items,
"search_items_batch": search_items_batch,
# ... 其他工具
}
return {name: build_tool(fn, name=name) for name, fn in raw_registry.items()}
# 2. 启动时初始化注册表
def init_tool_registry() -> None:
"""初始化工具注册表"""
global TOOL_REGISTRY
TOOL_REGISTRY.clear()
TOOL_REGISTRY.update(_build_tool_registry())
Sources: app/tools/factory.py, app/tools/__init__.py
灰色发布机制
系统支持基于用户的工具描述灰色发布,用于 A/B 测试、功能渐进式发布或用户个性化调整。
灰色发布架构:
graph TB
A[用户请求] --> B{是否有用户ID?}
B -->|是| C[加载用户特定描述]
B -->|否| D[加载基础描述]
C --> E[合并描述]
D --> F[直接使用基础描述]
E --> G[应用到工具对象]
F --> G
实现细节:
- 1. 描述覆盖:用户特定描述文件结构与基础文件相同,但只包含需要覆盖的工具
- 2. 合并策略:用户特定描述完全覆盖基础描述中的同名工具
- 3. 缓存机制:每个用户的描述独立缓存,避免相互影响
- 4. 性能优化:使用 LRU 缓存,最多缓存 128 个用户的描述
使用场景:
- - A/B 测试:测试不同工具描述对 LLM 行为的影响
- - 功能渐进式发布:先对特定用户发布新工具描述,观察效果后再全量发布
- - 用户个性化:根据用户偏好调整工具描述
- - 错误修复:快速修复特定用户的工具描述问题
管理接口:
灰色发布通过 Admin API 管理,具体接口在 app/routers/admin/gray.py 中定义:
- -
POST /admin/gray/rules:创建灰色发布规则 - -
PUT /admin/gray/rules/{rule_id}:更新灰色发布规则 - -
DELETE /admin/gray/rules/{rule_id}:删除灰色发布规则 - -
POST /admin/gray/preview:预览灰色发布效果
Sources: app/routers/admin/gray.py, tests/test_tool_description_gray.py
工具描述管理接口
系统提供完整的 Admin API 用于管理工具描述文件。
核心接口:
| 接口 | 方法 | 功能 | 说明 |
|---|---|---|---|
/admin/tool-descriptions | GET | 获取元信息 | 返回文件大小、修改时间、工具列表等 |
/admin/tool-descriptions/content | GET | 获取文件内容 | 返回完整的 TOML 文件内容 |
/admin/tool-descriptions/content | PUT | 更新文件内容 | 更新工具描述文件并清除缓存 |
/admin/tool-descriptions/reload | POST | 热重载 | 重新加载工具描述到运行时 |
/admin/tool-descriptions/history | GET | 获取历史版本 | 获取工具描述的历史版本列表 |
/admin/tool-descriptions/history/{version} | GET | 获取版本内容 | 获取指定历史版本的内容 |
/admin/tool-descriptions/rollback | POST | 回滚版本 | 将工具描述回滚到指定版本 |
热重载机制:
@router.post("/reload", summary="热重载工具描述到运行时")
async def reload_tool_descriptions(_: AdminRequired) -> JSONResponse:
"""热重载工具描述"""
try:
from app.agent import clear_runtime_tool_cache, reload_agents
from app.tools.factory import clear_tool_description_cache, list_tool_descriptions
# 清除缓存
clear_tool_description_cache()
clear_runtime_tool_cache()
# 重新加载代理
t0 = time.monotonic()
reload_agents()
elapsed_ms = int((time.monotonic() - t0) * 1000)
# 获取工具列表
tool_names = sorted(list_tool_descriptions().keys())
return JSONResponse({
"ok": True,
"tool_names": tool_names,
"tool_count": len(tool_names),
"elapsed_ms": elapsed_ms,
})
except Exception as exc:
logger.error("Admin tool description reload failed: %s", exc, exc_info=True)
raise HTTPException(status_code=500, detail=str(exc))
版本管理:
- - 自动备份:每次更新前自动备份当前版本
- - 历史版本:保留最近 10 个历史版本
- - 版本回滚:支持回滚到任意历史版本
- - 版本验证:回滚前验证内容格式的正确性
Sources: app/routers/admin/tool_descriptions.py
与技能系统的集成
工具描述与技能系统紧密集成,确保工具在正确的技能上下文中使用。
技能工具声明:
在 skill.toml 文件中,技能通过 tools 字段声明其拥有的工具:
# data/skills/buyer-wardrobe-skill/skill.toml
id = "buyer-wardrobe-skill"
name = "Buyer Wardrobe Skill"
description = "买手衣橱技能"
activation = "on_demand"
[metadata]
category = "business"
owner = "main-team"
[instructions]
zh = "skills/buyer_wardrobe.zh.txt"
en = "skills/buyer_wardrobe.en.txt"
# 声明技能拥有的工具
tools = [
"search_items",
"search_wardrobe",
"search_items_by_image_tool",
# ... 其他工具
]
工具门控机制: 系统通过工具门控机制确保工具只在正确的技能上下文中使用:
# app/dialog_state.py
def is_tool_allowed(
session_state: dict[str, Any] | None,
*,
tool_name: str,
owning_skills: list[str],
) -> bool:
"""检查工具是否在当前技能上下文中允许使用"""
dialog_state = get_dialog_state(session_state)
active_skill = dialog_state.get("active_skill")
# 如果没有活动技能,只允许常驻工具
if not active_skill:
return tool_name in _RESIDENT_TOOLS
# 如果工具属于活动技能,允许使用
if active_skill in owning_skills:
return True
# 如果工具不属于任何技能,作为常驻工具允许
if not owning_skills:
return True
return False
工具描述应用时机:
- 1. 技能激活时:当用户激活特定技能时,系统加载该技能的工具描述
- 2. 工具调用时:在工具调用前,系统检查工具是否在当前技能上下文中允许
- 3. 描述覆盖:技能可以覆盖基础工具描述,提供更具体的上下文指导
工具描述与技能的协同:
- - 技能上下文:工具描述应与技能的业务上下文保持一致
- - 参数指导:技能可以提供更具体的参数使用指导
- - 行为约束:技能可以约束工具在特定场景下的行为
- - 错误处理:技能可以定义特定场景下的错误处理策略
Sources: app/dialog_state.py, app/skills/loader.py
最佳实践
工具描述编写指南
- 1. 场景明确:清晰描述工具的使用场景,避免误用
- 2. 参数详细:详细说明必选和可选参数,包括格式要求和取值范围
- 3. 行为边界:明确工具的使用限制和替代方案
- 4. 返回格式:说明返回的数据结构和字段含义
- 5. 注意事项:包含重要的警告和最佳实践
灰色发布策略
- 1. 小范围测试:先对小范围用户测试新工具描述
- 2. 效果监控:监控工具描述变更对 LLM 行为的影响
- 3. 快速回滚:准备快速回滚机制,应对问题描述
- 4. 逐步发布:通过灰色发布逐步扩大新描述的适用范围
性能优化
- 1. 缓存利用:充分利用 LRU 缓存,避免重复文件读取
- 2. 延迟加载:只在需要时加载用户特定描述
- 3. 缓存清理:及时清理无效缓存,确保数据一致性
- 4. 监控指标:监控工具描述加载的性能指标
安全考虑
- 1. 输入验证:验证工具描述内容的格式和结构
- 2. 权限控制:限制工具描述的修改权限
- 3. 审计日志:记录工具描述的修改和访问日志
- 4. 备份策略:制定工具描述文件的备份策略
Sources: data/tools/descriptions.toml, app/tools/factory.py, app/routers/admin/tool_descriptions.py
下一步阅读
了解工具描述管理后,建议继续阅读以下相关文档:
- - 工具注册与工厂 - 了解工具如何注册到系统
- - 工具调用链路追踪 - 了解工具调用的监控和调试
- - 技能加载与管理 - 了解技能系统如何与工具集成
- - 技能工具门控机制 - 了解工具使用的权限控制机制
<a id="14-gong-ju-diao-yong-lian-lu-zhui-zong"></a>
---
工具调用链路追踪
- - Section: 架构与核心概念
- - Group: 工具系统
- - Source File:
14-gong-ju-diao-yong-lian-lu-zhui-zong.md
在复杂的 Agent 系统中,工具调用链路追踪是保障系统可观测性、调试效率和性能优化的关键基础设施。本文档详细介绍了 outfit_agent 中工具调用链路追踪的架构设计、核心组件和实现机制,帮助开发者理解如何追踪、监控和分析工具调用链路。
架构概览
工具调用链路追踪系统采用分层架构,从底层的 OpenTelemetry 集成到上层的实时监控和可视化,形成了完整的追踪链路。
graph TB
subgraph "应用层"
A[Agent 工具调用] --> B[工具包装层]
B --> C[追踪上下文绑定]
C --> D[追踪数据收集]
end
subgraph "追踪基础设施"
E[OpenTelemetry SDK]
F[Agno 追踪集成]
G[HyperDX 日志聚合]
end
subgraph "存储与查询"
H[PostgreSQL 追踪存储]
I[Redis 实时状态]
J[Admin API]
end
subgraph "可视化层"
K[Dashboard 追踪面板]
L[实时运行监控]
end
A --> E
E --> F
F --> H
D --> I
J --> K
J --> L
style A fill:#e1f5fe
style E fill:#f3e5f5
style H fill:#e8f5e8
style K fill:#fff3e0
Sources: app/agentos.py, app/observability.py
核心组件
1. 追踪上下文绑定
追踪上下文绑定是工具调用链路追踪的基础,它确保每次工具调用都关联到正确的会话、运行和用户上下文。
# 追踪上下文绑定示例
from outfit_biz.utils.agno_trace import bind_agno_context
with bind_agno_context(
session_id="session-123",
run_id="run-456",
user_id="user-789",
tool_name="search_items"
):
# 工具执行逻辑
pass
关键特性:
- - 会话级隔离:每个会话拥有独立的追踪上下文
- - 运行时绑定:上下文在工具调用时动态绑定
- - 自动清理:工具调用完成后自动清理上下文
Sources: app/tools/factory.py, app/img_gen_handler.py
2. 工具调用追踪
工具调用追踪通过 trace_tool_call 函数实现,它为每次工具调用创建详细的追踪记录。
# 工具调用追踪示例
from outfit_biz.utils.agno_trace import trace_tool_call
with trace_tool_call(
"search_items",
attributes={
"skill.id": "buyer-wardrobe-skill",
"scene.code": "outfit_recommendation",
"sop.stage": "await_search_confirmation",
"outfit.tool.source": "agent_tool",
},
input_value={"query": "夏季连衣裙"},
session_id="session-123",
run_id="run-456",
user_id="user-789"
) as traced:
# 执行工具逻辑
result = await search_items(query="夏季连衣裙")
traced.set_output(result)
追踪属性:
- - 技能信息:
skill.id、scene.code、sop.stage - - 工作流状态:
workflow.status、workflow.run_id - - 对话状态:
dialog.awaiting_confirmation、dialog.loaded_skills - - 工具来源:
outfit.tool.source(agent_tool/internal)
Sources: app/tools/factory.py, app/workflows/outfit_generation.py
3. 实时运行监控
实时运行监控通过 Redis 存储运行状态,提供工具调用的实时追踪能力。
sequenceDiagram
participant Agent as Agent 运行时
participant Hook as 工具钩子
participant Redis as Redis 存储
participant API as Admin API
participant Dashboard as 监控面板
Agent->>Hook: 工具调用开始
Hook->>Redis: 注册工具调用状态
Redis-->>API: 提供实时数据
API-->>Dashboard: 展示运行状态
Agent->>Hook: 工具调用完成
Hook->>Redis: 更新工具调用结果
Redis-->>API: 更新实时数据
API-->>Dashboard: 更新监控面板
核心功能:
- - 工具调用生命周期管理:
_start_tool_call和_finish_tool_call - - 状态快照:记录工具调用的输入、输出和状态
- - 心跳机制:通过心跳保持运行状态活跃
- - 自动清理:TTL 机制自动清理过期数据
Sources: app/live_runs.py, app/agent.py
实现细节
1. 工具包装层
工具包装层是追踪系统的核心,它为每个工具函数添加追踪和上下文绑定能力。
# 工具包装函数示例
def _wrap_tool_entrypoint(entrypoint, *, tool_name):
@wraps(entrypoint)
async def _async_wrapper(*args, **kwargs):
run_context = kwargs.get("run_context")
# 绑定追踪上下文
with bind_agno_context(
session_id=getattr(run_context, "session_id", None),
run_id=getattr(run_context, "run_id", None),
user_id=_resolve_tool_user_id(run_context),
tool_name=tool_name,
):
# 创建追踪记录
with trace_tool_call(
tool_name,
attributes=_build_trace_attributes(run_context),
input_value={"args_count": len(args)},
session_id=getattr(run_context, "session_id", None),
run_id=getattr(run_context, "run_id", None),
user_id=_resolve_tool_user_id(run_context),
) as traced:
# 执行门控检查
if not is_tool_allowed(...):
result = build_tool_gate_error(...)
traced.set_output(result)
return result
# 执行工具逻辑
result = await entrypoint(*args, **kwargs)
traced.set_output(result)
return result
return _async_wrapper
包装层职责:
- 1. 上下文提取:从
RunContext提取会话、运行和用户信息 - 2. 追踪绑定:绑定 OpenTelemetry 追踪上下文
- 3. 门控检查:执行工具访问权限和渠道限制检查
- 4. 结果记录:记录工具调用的输入和输出
Sources: app/tools/factory.py
2. 追踪属性构建
追踪属性构建函数 _build_trace_attributes 从对话状态中提取关键信息,为追踪记录提供丰富的上下文。
def _build_trace_attributes(run_context):
session_state = getattr(run_context, "session_state", None)
dialog_state = get_dialog_state(session_state)
pending_confirmation = dialog_state.get("pending_confirmation")
return {
"skill.id": dialog_state.get("active_skill"),
"scene.code": dialog_state.get("active_scene"),
"sop.stage": dialog_state.get("sop_stage"),
"workflow.status": dialog_state.get("outfit_status"),
"workflow.run_id": dialog_state.get("outfit_run_id"),
"dialog.awaiting_confirmation": bool(pending_confirmation),
"dialog.loaded_skills": dialog_state.get("loaded_skills") or [],
}
属性分类:
- - 技能上下文:当前激活的技能、场景和 SOP 阶段
- - 工作流状态:穿搭生成工作流的状态和运行 ID
- - 对话状态:是否等待确认、已加载的技能列表
Sources: app/tools/factory.py
3. LLM 请求日志
LLM 请求日志通过 LoggedOpenAIChat 类实现,记录详细的 LLM 调用信息。
# LLM 请求日志示例
class LoggedOpenAIChat(OpenAIChat):
async def ainvoke(self, messages, assistant_message, **kwargs):
# 构建请求快照
snapshot = self._build_request_snapshot(
messages=messages,
response_format=response_format,
tools=tools,
tool_choice=tool_choice,
run_response=run_response,
)
# 记录请求开始
self._log_request_start("async", snapshot, client_summary)
# 执行请求
response = await super().ainvoke(...)
# 记录请求完成
self._log_request_end("async", snapshot, elapsed_ms, response)
return response
日志内容:
- - 请求摘要:消息数量、角色分布、内容大小
- - 模型信息:模型 ID、基础 URL、超时设置
- - 性能指标:请求耗时、响应大小
- - 错误信息:请求失败时的详细错误
Sources: app/llm_logging.py
配置与使用
1. 环境变量配置
追踪系统依赖以下环境变量进行配置:
# HyperDX 监控配置
HYPERDX_API_KEY=your_hyperdx_api_key
OTEL_SERVICE_NAME=outfit-agent
OTEL_EXPORTER_OTLP_ENDPOINT=https://in-otel.hyperdx.io
# Redis 配置(用于实时运行监控)
REDIS_URL=redis://localhost:6379/0
# 数据库配置(用于追踪存储)
DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/outfit
Sources: .env.example, config.yml
2. 追踪系统初始化
追踪系统在应用启动时自动初始化:
# 追踪系统初始化流程
def init_hyperdx_monitoring():
config = load_hyperdx_config()
if config is None:
return False
# 初始化日志导出器
_ensure_log_exporter(config)
logger.info("HyperDX monitoring enabled for logs only")
return True
# AgentOS 追踪初始化
def ensure_agno_db_tracing(db):
provider = trace_api.get_tracer_provider()
# 添加数据库跨度导出器
provider.add_span_processor(
BatchSpanProcessor(DatabaseSpanExporter(db=db))
)
# 初始化 Agno 仪表化
instrumentor = AgnoInstrumentor()
instrumentor.instrument(tracer_provider=provider)
Sources: app/observability.py, app/agentos.py
3. 工具注册与追踪
工具注册时自动添加追踪包装:
# 工具注册示例
def build_tool(entrypoint, *, name=None):
tool_name = name or entrypoint.__name__
# 包装工具入口点
wrapped = _wrap_tool_entrypoint(entrypoint, tool_name=tool_name)
# 创建工具函数
tool = Function.from_callable(wrapped, name=tool_name)
tool.description = get_tool_description(tool_name)
# 设置工具属性
if getattr(entrypoint, "__tool_stop_after_tool_call__", False):
tool.stop_after_tool_call = True
tool.show_result = True
return tool
Sources: app/tools/factory.py
监控与调试
1. Admin API 接口
Admin API 提供丰富的追踪查询接口:
| 接口 | 方法 | 描述 |
|---|---|---|
/traces/sessions | GET | 分页列出会话(支持多维度过滤) |
/traces/sessions/{session_id} | GET | 获取会话详情(包含所有运行摘要) |
/traces/runs/{run_id} | GET | 获取单次运行的完整跨度树 |
/traces/live-runs | GET | 获取当前进程内正在运行的运行列表 |
/traces/live-runs/{run_id} | GET | 获取单个实时运行的详细信息 |
/traces/runs/{run_id}/retry-image-generation | POST | 重新入队生图任务 |
查询参数示例:
# 按用户 ID 过滤会话
GET /traces/sessions?user_id=42&limit=20&page=1
# 获取运行详情(包含基础设施跨度)
GET /traces/runs/run-123?include_infra_spans=true
# 获取实时运行列表
GET /traces/live-runs?session_id=session-456&limit=50
Sources: app/routers/admin/traces.py
2. Dashboard 追踪面板
Dashboard 提供可视化的追踪面板,支持以下功能:
会话列表视图:
- - 会话 ID、用户 ID、创建时间
- - 追踪数量、错误数量、最后追踪时间
- - Agent ID、Team ID、渠道信息
- - 灰度配置摘要
运行详情视图:
- - 运行状态、耗时、Agent 信息
- - 工具调用列表(名称、状态、耗时)
- - LLM 请求摘要(消息数量、Token 使用)
- - 对话状态快照(技能、场景、SOP 阶段)
实时运行监控:
- - 当前运行状态、工具调用状态
- - 实时更新(通过轮询或 WebSocket)
- - 工具调用输入/输出预览
- - 性能指标和错误信息
Sources: dashboard/src/pages/TraceInspector.tsx, dashboard/src/api/traces.ts
3. 追踪数据查询示例
查询用户会话历史:
SELECT
s.session_id,
s.user_id,
s.created_at,
s.updated_at,
COUNT(t.trace_id) as trace_count,
COUNT(CASE WHEN t.status = 'error' THEN 1 END) as error_count
FROM agno.agno_sessions s
LEFT JOIN agno.agno_traces t ON s.session_id = t.session_id
WHERE s.user_id = '42'
GROUP BY s.session_id, s.user_id, s.created_at, s.updated_at
ORDER BY s.updated_at DESC
LIMIT 20;
查询工具调用详情:
SELECT
sp.span_id,
sp.name as tool_name,
sp.attributes->>'outfit.tool.source' as tool_source,
sp.attributes->>'skill.id' as skill_id,
sp.attributes->>'sop.stage' as sop_stage,
sp.start_time,
sp.end_time,
EXTRACT(EPOCH FROM (sp.end_time - sp.start_time)) * 1000 as duration_ms
FROM agno.agno_spans sp
WHERE sp.trace_id = 'trace-123'
AND sp.span_kind = 'tool'
ORDER BY sp.start_time;
最佳实践
1. 工具开发规范
必须遵循的规范:
- 1. 使用工具包装:所有工具必须通过
build_tool函数注册 - 2. 提供清晰描述:在
tools/descriptions.toml中为每个工具提供描述 - 3. 处理错误:工具内部错误应被捕获并记录到追踪系统
- 4. 设置超时:为长时间运行的工具设置合理的超时时间
工具描述示例:
# tools/descriptions.toml
[tools]
search_items = "根据关键词搜索商品,支持多维度筛选"
generate_outfit = "生成穿搭方案,包含图片和描述"
send_text_message = "发送文本消息给用户"
Sources: data/tools/descriptions.toml
2. 追踪属性使用
推荐使用的追踪属性:
- -
skill.id:当前激活的技能 ID - -
scene.code:当前场景代码 - -
sop.stage:SOP 阶段 - -
outfit.tool.source:工具来源(agent_tool/internal) - -
workflow.status:工作流状态 - -
dialog.awaiting_confirmation:是否等待确认
避免的属性:
- - 敏感信息:用户密码、Token、密钥
- - 大量数据:完整的请求/响应体
- - 动态变化:频繁变化的计数器
3. 性能优化建议
追踪性能优化:
- 1. 采样率控制:在生产环境设置合理的采样率
- 2. 批量处理:使用批量处理器减少数据库写入
- 3. 异步导出:确保追踪导出不阻塞主业务流程
- 4. 索引优化:为常用查询字段创建数据库索引
内存使用优化:
- 1. 及时清理:确保追踪上下文及时清理
- 2. 限制大小:限制单个追踪记录的大小
- 3. 压缩存储:对大型追踪数据使用压缩存储
故障排查
1. 常见问题排查
追踪数据丢失:
- 1. 检查环境变量配置:
HYPERDX_API_KEY、OTEL_SERVICE_NAME - 2. 检查数据库连接:确保 PostgreSQL 连接正常
- 3. 检查 Redis 连接:确保 Redis 连接正常
- 4. 查看应用日志:检查追踪初始化错误
实时监控延迟:
- 1. 检查 Redis 性能:确保 Redis 响应时间正常
- 2. 检查心跳间隔:调整
live_run_heartbeat_interval_seconds - 3. 检查 TTL 设置:调整
live_run_redis_ttl_seconds
工具调用未追踪:
- 1. 检查工具注册:确保工具通过
build_tool注册 - 2. 检查包装层:确保
_wrap_tool_entrypoint正常工作 - 3. 检查门控逻辑:确保工具未被门控拦截
2. 调试工具
查看追踪上下文:
from outfit_biz.utils.agno_trace import get_agno_trace_context
# 获取当前追踪上下文
ctx = get_agno_trace_context()
print(f"Session ID: {ctx.session_id}")
print(f"Run ID: {ctx.run_id}")
print(f"User ID: {ctx.user_id}")
print(f"Tool Name: {ctx.tool_name}")
检查工具注册:
from app.tools.factory import has_tool, get_tool_name
# 检查工具是否注册
tools = get_all_tools()
print(f"Tool 'search_items' registered: {has_tool(tools, 'search_items')}")
# 获取工具名称
for tool in tools:
print(f"Tool: {get_tool_name(tool)}")
查看实时运行状态:
from app.live_runs import list_live_run_summaries
# 获取实时运行列表
runs = await list_live_run_summaries(session_id="session-123")
for run in runs:
print(f"Run ID: {run['run_id']}")
print(f"Status: {run['status']}")
print(f"Active Tools: {run['active_tool_names']}")
扩展与定制
1. 自定义追踪属性
可以通过扩展 _build_trace_attributes 函数添加自定义追踪属性:
def _build_trace_attributes(run_context):
# 原有属性
attributes = {
"skill.id": dialog_state.get("active_skill"),
"scene.code": dialog_state.get("active_scene"),
# ... 其他属性
}
# 自定义属性
attributes.update({
"custom.business_type": "outfit_recommendation",
"custom.user_segment": "premium",
"custom.ab_test_group": "group_a",
})
return attributes
2. 集成第三方监控
可以扩展追踪系统以集成第三方监控平台:
# 集成 Jaeger
from opentelemetry.exporter.jaeger import JaegerExporter
jaeger_exporter = JaegerExporter(
agent_host_name="localhost",
agent_port=6831,
)
provider.add_span_processor(
BatchSpanProcessor(jaeger_exporter)
)
# 集成 Zipkin
from opentelemetry.exporter.zipkin import ZipkinExporter
zipkin_exporter = ZipkinExporter(
endpoint="http://localhost:9411/api/v2/spans",
)
provider.add_span_processor(
BatchSpanProcessor(zipkin_exporter)
)
3. 自定义 Dashboard 组件
可以扩展 Dashboard 以添加自定义追踪组件:
// 自定义追踪面板组件
const CustomTracePanel = ({ sessionId }) => {
const [traces, setTraces] = useState([]);
useEffect(() => {
// 加载追踪数据
tracesApi.getSession(sessionId).then(data => {
setTraces(data.runs);
});
}, [sessionId]);
return (
<div>
<h3>自定义追踪面板</h3>
{traces.map(trace => (
<div key={trace.run_id}>
<span>{trace.run_id}</span>
<span>{trace.status}</span>
<span>{trace.duration_ms}ms</span>
</div>
))}
</div>
);
};
总结
工具调用链路追踪系统是 outfit_agent 可观测性的核心组成部分,它通过多层次的追踪机制,提供了从工具调用到 LLM 请求的完整链路追踪能力。系统的主要特点包括:
- 1. 完整的追踪链路:从工具调用到 LLM 请求的完整追踪
- 2. 实时监控能力:基于 Redis 的实时运行状态监控
- 3. 丰富的查询接口:Admin API 提供多维度的追踪查询
- 4. 可视化支持:Dashboard 提供直观的追踪面板
- 5. 灵活的扩展性:支持自定义追踪属性和第三方集成
通过合理使用追踪系统,开发者可以快速定位性能瓶颈、调试工具调用问题,并优化 Agent 系统的整体性能。
下一步
了解工具调用链路追踪后,建议继续阅读:
- - 工具注册与工厂 - 了解工具如何注册和管理
- - 工具描述管理 - 了解工具描述的配置和管理
- - 链路追踪与 Trace - 了解更广泛的追踪系统架构
- - 系统监控与健康检查 - 了解系统监控和健康检查机制
<a id="15-xiao-xi-ru-zhan-chu-li-liu-cheng"></a>
---
消息入站处理流程
- - Section: 核心模块详解
- - Group: 消息处理与渠道
- - Source File:
15-xiao-xi-ru-zhan-chu-li-liu-cheng.md
本文档系统梳理从用户发送消息到 Agent 生成回复的完整入站链路。覆盖消息接收、安全审计、并发调度、Agent 执行及回复持久化等关键阶段,帮助开发者理解系统如何在多渠道、多消息类型场景下实现有序处理。
概览:三层架构
消息入站处理采用渠道适配层 → 调度层 → 执行层的三层分离架构。渠道适配层负责协议解密和消息格式标准化;调度层管理单会话内的消息并发和队列;执行层完成 Agent 调用与回复持久化。
flowchart TB
subgraph 渠道适配层
WX["微信客服<br/>routers/wechat.py"]
MP["小程序 WebSocket<br/>routers/miniapp/ws.py"]
CHAT["Chat API<br/>routers/chat.py"]
end
subgraph 调度层
DISPATCH["Session Dispatcher<br/>messaging/session_dispatcher.py"]
SHORTCUT["快捷方式匹配<br/>messaging/inbound_shortcuts.py"]
NORM["消息规范化<br/>messaging/inbound.py"]
end
subgraph 执行层
SVC["Agent Service<br/>messaging/agent_service.py"]
PIPE["图像管道<br/>messaging/image_pipeline.py"]
AGENT["Agent 运行时<br/>agent.py"]
DISP_FILT["显示过滤器<br/>messaging/display_filters.py"]
end
WX --> DISPATCH
MP --> DISPATCH
CHAT --> AGENT
DISPATCH --> SHORTCUT
DISPATCH --> NORM
NORM --> PIPE
DISPATCH --> SVC
SVC --> AGENT
AGENT --> DISP_FILT
三层之间通过回调协议(Protocol) 解耦:渠道层只需实现 TextHandler、ImagePreprocessor、ImageErrorHandler、FollowupArbiter 四个接口,即可接入统一调度。
Sources: session_dispatcher.py, \_\_init\_\_.py
阶段一:渠道接收与解密
每个渠道入口对接各自的传输协议,但最终都标准化为 (content, image_ref) 两种消息类型进入调度层。
微信客服渠道
微信客服消息通过同步拉取模式获取。系统轮询 sync_msg API 拉取新消息批次,对每条消息逐一处理。
接收流程:POST /wechat/callback → XML 解密 → _process_kf_event → async_sync_msg 批量拉取 → _handle_synced_message 逐条处理。
在 _handle_synced_message 中,每条消息依次经历以下关卡:
| 步骤 | 检查点 | 失败行为 |
|---|---|---|
| 1 | 消息去重 — Redis msgid 缓存,TTL 24h | 静默跳过 |
| 2 | 欢迎事件处理 — welcome_code + 新用户检测 | 发送欢迎语后返回 |
| 3 | 用户身份解析 — ensure_user_oauth 创建/获取 db_user_id | 发送错误回复后返回 |
| 4 | 手动客服拦截 — queue_manual_service_message_if_active | 消息进入人工客服队列 |
| 5 | 访问控制 — check_inbound_access 黑白名单策略 | 发送拒绝提示后返回 |
| 6 | 文本内容审核 — ensure_text_content_allowed | 发送违规提示后返回 |
| 7 | 快捷方式匹配 — resolve_inbound_shortcut | 特殊快捷动作直接返回 |
| 8 | 分发到调度器 — dispatch_text 或 dispatch_image | 进入并发调度 |
语音消息在进入文本流程前会先通过阿里云语音转文字服务(_transcribe_wechat_voice_message)转为文本,后续路径与文本消息一致。
Sources: wechat.py, message_access_control.py, message_interception.py
小程序 WebSocket 渠道
小程序通过 WebSocket 长连接接收消息。消息以 JSON 帧传输,无需 XML 解密,但同样需要经过文本审核和访问控制检查。消息通过 dispatch_text 进入统一调度。
Sources: ws.py
Chat API 渠道
/api/chat 端点处理 HTTP 请求,支持流式(SSE)和非流式两种模式。此路径不经过 session_dispatcher 的并发管理,而是直接调用 run_agent_with_memory 或 run_agent_stream_with_memory,但仍需先解析 agent_instruction 和检查 inbound_shortcut。
Sources: chat.py
阶段二:消息规范化
app/messaging/inbound.py 提供渠道无关的消息标准化能力,将不同渠道的原始数据统一为 InboundMessagePart 列表。
flowchart LR
subgraph 输入格式
TEXT["纯文本消息"]
MEDIA["微信 media_id"]
STORED["已存储 image_id"]
URL["远程图片 URL"]
end
subgraph 规范化
PART["InboundMessagePart"]
REF["ImageInputRef"]
end
TEXT --> |"from_text()"| PART
MEDIA --> |"ImageInputRef(kind=wechat_media_id)"| REF
STORED --> |"ImageInputRef(kind=stored_image_id)"| REF
URL --> |"ImageInputRef(kind=remote_url)"| REF
REF --> |"from_image()"| PART
ImageInputRef 统一三种图片来源引用方式,携带 kind 字段区分来源:
| kind | 必需字段 | 用途 |
|---|---|---|
wechat_media_id | media_id | 微信客服图片,需通过 media_fetcher 下载 |
stored_image_id | image_id(正整数) | 数据库已存储图片,直接查询 |
remote_url | url | 外部 URL 图片,直接下载 |
prepare_image_message 函数根据 ImageInputRef.kind 分发到不同处理路径:已存储图片直接构建 ImagePipelineResult;其他来源先获取字节流再走图像管道。
Sources: inbound.py
阶段三:快捷方式匹配
在消息进入 Agent 调用之前,系统首先尝试匹配入站快捷方式(inbound_shortcuts)。快捷方式是一组预定义规则,用于对特定关键词返回固定回复,避免触发完整的 Agent 调用链路。
匹配逻辑在两个层级执行:
- 1. 调度层 — 渠道路由中调用
resolve_inbound_shortcut,匹配后直接发送快捷回复(如微信客服的wechat_demo_miniprogram类型) - 2. 执行层 —
agent_service.py中再次调用resolve_inbound_shortcut,匹配后通过deliver_shortcut_messages发送富媒体消息(图文卡片、小程序卡片等)
规则定义在 data/i18n/inbound_shortcuts.json 中,支持按渠道过滤(channels)、精确匹配(match_type: exact),以及引用国际化文本(trigger_text_refs)。
| 字段 | 说明 |
|---|---|
rule_id | 规则标识 |
channels | 适用渠道列表,"*" 表示所有渠道 |
match_type | 匹配方式,当前仅支持 exact |
trigger_texts | 触发文本列表 |
trigger_text_refs | 间接引用 i18n 文本 |
action.type | text(纯文本)、messages(富消息)、wechat_demo_miniprogram(特殊) |
Sources: inbound_shortcuts.py
阶段四:会话并发调度
session_dispatcher 是消息入站处理的核心调度器,解决一个关键问题:当用户快速连续发送多条消息时,如何保证 Agent 有序处理而不产生响应冲突。
并发模型
stateDiagram-v2
[*] --> 空闲
空闲 --> 文本任务运行 : dispatch_text()
空闲 --> 图像处理中 : dispatch_image()
空闲 --> 排队等待 : 队列非空
文本任务运行 --> 空闲 : 任务完成 → drain_queue()
文本任务运行 --> 文本任务运行 : 新文本到达 → 取消旧任务/合并内容
文本任务运行 --> 排队等待 : 新图像到达 → 入队
图像处理中 --> 排队等待 : 新消息到达 → 入队
图像处理中 --> 文本任务运行 : 图像完成 → schedule_text_task
排队等待 --> 文本任务运行 : drain_queue() 弹出文本
排队等待 --> 图像处理中 : drain_queue() 弹出图像
排队等待 --> 空闲 : 队列清空
状态管理
调度器使用四个模块级字典跟踪每个会话的状态:
| 字典 | 键 | 值 | 用途 |
|---|---|---|---|
_text_tasks | session_id | asyncio.Task | 当前正在运行的文本 Agent 任务 |
_text_task_contents | session_id | str | 传给当前任务的内容(用于合并) |
_image_processing | session_id | asyncio.Event | 图像处理进行中标记 |
_queue_draining | session_id(集合) | — | 防止 drain_queue 重入 |
所有状态仅从 asyncio 事件循环单线程修改,无需加锁。
文本消息调度(dispatch_text)
当新文本消息到达时,调度器根据当前状态做出三种决策:
- 1. 有图像在飞或队列非空 → 消息入队,等待后续 drain
- 2. 有旧文本任务运行 → 调用
FollowupArbiter仲裁,支持四种策略:
- -
interrupt(默认):取消旧任务,合并内容为新任务 - -
fresh:取消旧任务,以全新内容启动 - -
parallel:保留旧任务,新消息入队 - -
handled:旧任务继续,新消息已被拦截处理
- 3. 无任务运行 → 直接调度新任务
新任务创建后会进入 0.3 秒防抖窗口(debounce),在此期间如果被更新的任务取消,就不会调用 Agent。防抖通过 dispatch_guard 中的 ContextVar 实现任务身份验证,确保过期任务不会误执行。
Sources: session_dispatcher.py, dispatch_guard.py
图像消息调度(dispatch_image)
图像消息具有最高优先级:
- 1. 如果有正在运行的文本任务 → 取消它,保留其内容加入图像批次
- 2. 标记图像处理开始(
mark_image_in_flight) - 3. 启动后台任务
_run_accumulated_image_batch
图像批处理循环会持续从队列中弹出消息,预处理所有图像,最后将合并的文本+图像结果交给文本任务路径。如果图像管道失败(如内容审核不通过),会通过 on_image_error 回调通知渠道层。
Sources: session_dispatcher.py
阶段五:图像管道
图像管道(image_pipeline)处理图像从接收到可被 Agent 使用的完整生命周期:
flowchart LR
A["原始图像字节"] --> B["内容审核<br/>ensure_image_content_allowed"]
B --> C["上传 OSS + 去重检测<br/>ingest_user_uploaded_image"]
C --> D{去重?}
D -->|是| E["返回已有 image_id<br/>is_duplicate=True"]
D -->|否| F["保存图像 + 图像描述生成"]
F --> G["构建 agent_message"]
G --> H["ImagePipelineResult"]
E --> H
去重阈值默认为 0.97(余弦相似度),超过此阈值的图像被视为重复,跳过 Agent 调用。去重检测通过 ingest_user_uploaded_image 在数据库级别完成。
ImagePipelineResult 包含三个关键字段:
- -
image_id— 数据库中的图像 ID - -
agent_message— 面向 Agent 的描述文本,格式如[用户发送了一张图片,图片ID=123] - -
user_message— 面向持久化的结构化 JSON 载荷
Sources: image_pipeline.py, agent_messages.py
阶段六:Agent 执行(run_agent_turn)
agent_service.py 中的 run_agent_turn 函数是 Agent 调用的核心编排器,负责消息持久化、Agent 运行、回复发送的全流程管理。
完整执行流程
flowchart TB
START["run_agent_turn"] --> CONV["获取或创建会话<br/>get_or_create_conversation"]
CONV --> PERSIST_USER["持久化用户消息<br/>save_user_message"]
PERSIST_USER --> REG_REPLY["注册回复状态<br/>register_pending_reply_keys"]
REG_REPLY --> BUILD_CTX["构建 ToolContext"]
BUILD_CTX --> LOAD_IMG["加载模型图像 + 历史图像"]
LOAD_IMG --> SHORTCUT_CHECK{快捷方式<br/>匹配?}
SHORTCUT_CHECK -->|是| DELIVER_SHORTCUT["deliver_shortcut_messages"]
SHORTCUT_CHECK -->|否| STALE_CHECK_1{是否过期?}
DELIVER_SHORTCUT --> STALE_CHECK_2{是否过期?}
STALE_CHECK_1 -->|过期| DROP_1["丢弃结果"]
STALE_CHECK_1 -->|有效| RUN_AGENT["run_agent_with_memory"]
STALE_CHECK_2 -->|过期| DROP_2["丢弃结果"]
STALE_CHECK_2 -->|有效| SEND_REPLY_CHECK
RUN_AGENT --> STALE_CHECK_3{是否过期?}
STALE_CHECK_3 -->|过期| DROP_3["丢弃结果"]
STALE_CHECK_3 -->|有效| SEND_REPLY_CHECK{send_reply<br/>回调?}
SEND_REPLY_CHECK -->|有| SEND["发送回复到渠道"]
SEND_REPLY_CHECK -->|无| PERSIST_REPLY["持久化助手消息"]
SEND --> PERSIST_REPLY
PERSIST_REPLY --> MARK_SENT["标记回复已发送<br/>mark_session_reply_sent"]
MARK_SENT --> RESULT["返回 AgentTurnResult"]
过期任务检测
在 Agent 执行过程中,每个关键步骤之间都会调用 is_current_dispatch_task(session_id) 检查当前任务是否仍是最新的。如果在 Agent 运行期间用户发送了新消息,旧任务会被取消或被更新的任务取代,此时旧任务的输出会被静默丢弃,避免向用户发送过时回复。
回复状态跟踪
reply_state 模块(messaging/reply_state.py)通过 Redis 跟踪每条消息的回复状态,防止重复回复。关键机制:
- -
register_latest_user_message— 记录最新用户消息 ID - -
register_pending_reply_keys— 注册待回复的消息标识 - -
mark_session_reply_sent— 标记回复已完成(支持phase阶段标记)
Sources: agent_service.py, reply_state.py
阶段七:显示过滤与回复发送
Agent 生成的回复在发送给用户前会经过显示过滤器(display_filters.py)处理,移除内部推理标记(如 </nuva_thought> 之前的内容)。这确保用户只看到最终结论,而非 Agent 的中间思考过程。
过滤规则为硬编码的元组:
- -
strip_before_including— 移除指定标记及其之前的所有内容 - -
remove_between_including— 移除两个标记之间的内容(含标记)
渠道层在 send_reply 回调中应用过滤后再发送。以微信客服为例,过滤后的文本还会经过 rewrite_wechat_mp_links_for_chat 将小程序链接转为微信可解析格式,再通过 build_wechat_reply_delivery 决定是以纯文本还是菜单消息形式发送。
Sources: display_filters.py, wechat.py
后续消息仲裁(Followup Arbiter)
当用户在 Agent 处理过程中连续发送消息时,FollowupArbiter 决定如何处理新消息。微信渠道实现了自定义仲裁器 arbitrate_wechat_followup(wechat_followup_intent.py),它结合 Agno 运行时快照来判断:
- - Agent 是否有活跃的远程任务(如图像生成工作流)
- - 是否应中断当前任务、合并内容、还是并行排队
仲裁结果为 FollowupDecision,包含 action(interrupt/parallel/handled/fresh)、可选的 replacement_content 和 message_records。
Sources: wechat_followup_intent.py, session_dispatcher.py
关键设计模式总结
| 模式 | 应用位置 | 解决的问题 |
|---|---|---|
| Protocol 解耦 | TextHandler、ImagePreprocessor 等 | 渠道层与调度层解耦,新渠道只需实现协议接口 |
| 防抖(Debounce) | _schedule_text_task 中 0.3s 延迟 | 用户快速连续输入时合并消息,减少 Agent 调用 |
| 任务身份验证 | dispatch_guard.py 中的 ContextVar | 确保过期异步任务不会误执行 |
| 队列 drain | drain_queue 循环 | 图像处理完成后自动恢复被阻塞的文本消息 |
| 幂等去重 | Redis msgid 缓存 | 微信消息重传不会重复处理 |
| 双层快捷方式 | 路由层 + Agent 服务层 | 简单请求快速响应,避免完整 Agent 链路开销 |
相关文档
- - 微信渠道的详细集成细节参见 微信客服与小程序集成
- - 会话与回复状态的深入设计参见 会话分发与回复状态
- - Agent 架构和技能系统参见 单主 Agent 架构
- - Dialog State 的设计参见 Dialog State 设计
- - 工具系统的注册与工厂参见 工具注册与工厂
<a id="16-wei-xin-ke-fu-yu-xiao-cheng-xu-ji-cheng"></a>
---
微信客服与小程序集成
- - Section: 核心模块详解
- - Group: 消息处理与渠道
- - Source File:
16-wei-xin-ke-fu-yu-xiao-cheng-xu-ji-cheng.md
本页文档详解系统与微信生态的两条集成通道——企业微信客服(KEFU)和微信小程序(Mini Program)。二者共享统一的 Agent 核心和消息调度框架,但在入站协议、出站投递和用户身份体系上各具特点。理解两条通道的差异与共性,是进行渠道定制或排查跨渠道问题的前提。
架构总览
系统通过两条并行的微信通道与用户交互,它们在入口层分流,但汇聚到同一套消息调度器和 Agent 运行时。
flowchart TB
subgraph WeChat["微信生态"]
UserK["企业微信客服用户"]
UserM["小程序用户"]
end
subgraph Entry["入口层"]
WB["POST /wechat/callback<br/>XML Webhook"]
MC["POST /api/wechat/chat<br/>HTTP REST"]
MW["WS /api/wechat/ws<br/>WebSocket"]
end
subgraph WechatModule["app/wechat/ 模块"]
Crypto["crypto.py<br/>AES 加解密"]
API["api.py<br/>企业微信 API 封装"]
MenuMD["menu_markdown.py<br/>菜单消息渲染"]
MiniprogramRoutes["miniprogram_routes.py<br/>小程序路由解析"]
end
subgraph Messaging["app/messaging/ 统一调度层"]
Dispatcher["session_dispatcher.py<br/>会话消息调度器"]
Inbound["inbound.py<br/>入站消息归一化"]
AgentSvc["agent_service.py<br/>Agent 执行服务"]
MiniappWS["miniapp_ws.py<br/>WebSocket 连接注册"]
ReplyState["reply_state.py<br/>回复状态跟踪"]
end
subgraph Routers["路由层"]
WechatRouter["routers/wechat.py<br/>客服回调路由"]
MiniappChatRouter["routers/miniapp/chat.py<br/>小程序聊天路由"]
MiniappWsRouter["routers/miniapp/ws.py<br/>小程序 WS 路由"]
end
UserK -->|"XML/加密"| WB
UserM -->|"JSON/Token"| MC
UserM -->|"WS 连接"| MW
WB --> WechatRouter
MC --> MiniappChatRouter
MW --> MiniappWsRouter
WechatRouter --> Crypto
WechatRouter --> API
WechatRouter --> Dispatcher
MiniappChatRouter --> AgentSvc
MiniappWsRouter --> MiniappWS
MiniappWsRouter --> AgentSvc
API -->|"send_msg"| WeChat
MiniappWS -->|"推送事件"| UserM
Dispatcher --> Inbound
Dispatcher --> AgentSvc
两条通道的核心差异如下表所示:
| 维度 | 企业微信客服 (KEFU) | 微信小程序 (Mini Program) |
|---|---|---|
| 入站协议 | XML Webhook(AES 加密) | HTTP REST + WebSocket |
| 出站协议 | kf/send_msg API | WebSocket 推送 + HTTP 回复 |
| 认证方式 | access_token(Corp ID + Secret) | 用户 JWT Token |
| 会话标识 | external_userid + open_kfid | user_id(数据库主键) |
| 渠道标识 | RegisterChannel.WECHAT_KEFU | RegisterChannel.WECHAT_MINIAPP |
| 消息类型支持 | text / image / voice / event / msgmenu | text / image(multipart) |
| 并发控制 | Redis 去重 + 消息配额 | 会话调度器(通用) |
企业微信客服通道
企业微信客服通道通过 Webhook 回调接收消息,经 AES 解密后进入统一调度流程。系统以"拉取模式"同步客服消息——收到事件通知后调用 kf/sync_msg 拉取实际消息体。
Webhook 回调处理
企业微信客服消息的入口是 POST /wechat/callback。该端点负责两件事:URL 验证(GET 请求)和消息事件接收(POST 请求)。事件到达后经过签名验证、AES 解密和 XML 解析,仅处理 kf_msg_or_event 类型的事件。
消息处理流程如下:
sequenceDiagram
participant WX as 企业微信服务器
participant Router as /wechat/callback
participant Sync as sync_msg 拉取
participant Handler as _handle_synced_message
participant Dispatcher as session_dispatcher
participant Agent as Agent 运行时
participant API as kf/send_msg
WX->>Router: POST XML(加密事件)
Router->>Router: AES 解密 + 签名验证
Router->>Sync: async_sync_msg(cursor)
Sync-->>Router: msg_list + next_cursor
loop 每条消息
Router->>Handler: _handle_synced_message(msg)
Handler->>Handler: 去重(Redis msgid)
Handler->>Handler: ensure_user_oauth(创建/关联用户)
Handler->>Handler: 内容审核 + 权限校验
Handler->>Dispatcher: dispatch_text / dispatch_image
Dispatcher->>Agent: run_agent_with_memory
Agent-->>Dispatcher: reply
Dispatcher->>API: send_text_msg / send_menu_msg
end
Router-->>WX: "success"
关键设计点在于消息去重和用户身份映射。系统使用 Redis 以 wechat:kefu:msg:{msgid} 为键做 24 小时去重窗口;同时通过 ensure_user_oauth 将企业微信的 external_userid 映射为数据库用户 ID,支持 unionid 跨渠道关联。
Sources: routers/wechat.py, routers/wechat.py, wechat/api.py
消息加解密
企业微信使用 AES-CBC 模式对回调消息进行加密。WXBizMsgCrypt 类封装了加解密逻辑,使用 encoding_aes_key 生成密钥,corp_id 作为身份标识参与校验。签名验证采用 SHA1 哈希,将 token、timestamp、nonce、encrypt 排序后拼接比较。
Sources: wechat/crypto.py
用户欢迎语与小程序卡片
首次进入客服会话的用户会触发 welcome_code 事件。系统通过 send_msg_on_event API 发送欢迎消息,支持纯文本和 msgmenu(菜单消息)两种格式。菜单消息中可以嵌入小程序卡片,引导用户从客服场景跳转到小程序获得完整功能体验。
欢迎语的格式由 wechat_welcome 配置项控制,支持三种菜单项类型:
| 菜单项类型 | 用途 | 关键字段 |
|---|---|---|
click | 触发快捷文本回复 | content |
view | 打开外部链接 | url, content |
miniprogram | 打开小程序页面 | appid, pagepath, content |
小程序卡片发送需要 thumb_media_id(缩略图素材 ID)。系统支持自动上传和缓存缩略图——当配置了 WECHAT_MINIPROGRAM_IMAGE_URL 时,会自动上传图片并缓存 media_id 到 Redis(TTL 2.5 天),过期后自动刷新。
Sources: routers/wechat.py, routers/wechat.py, wechat/api.py
消息发送能力
企业微信客服 API(kf/send_msg)支持多种消息类型,系统通过 app/wechat/api.py 封装了同步和异步两套接口:
| 消息类型 | API 函数 | 用途 |
|---|---|---|
| 文本 | async_send_text_msg | Agent 回复、错误提示 |
| 图片 | async_send_image_msg | 穿搭结果图 |
| 菜单 | async_send_menu_msg | 交互式菜单回复 |
| 小程序卡片 | async_send_miniprogram_msg | 引导跳转小程序 |
所有 API 调用都经过统一的 _call_wechat_api_async 包装,内置性能日志(elapsed_ms)和错误码解析。access_token 通过 Redis 缓存(TTL 7000 秒),避免重复请求。
系统还支持将 Agent 输出中的 Markdown 菜单语法自动转换为微信 msgmenu 格式。menu_markdown.py 负责解析 选项文本 语法,生成包含 click、view、miniprogram 等菜单项的消息体。
Sources: wechat/api.py, wechat/api.py, wechat/menu_markdown.py
语音消息处理
当用户发送语音消息时,系统下载原始音频(AMR 格式),调用阿里云语音识别服务(transcribe_audio_bytes)转写为文本,然后作为普通文本消息进入调度流程。转写失败时返回错误提示。
Sources: routers/wechat.py, routers/wechat.py
图片处理与提示
当用户发送图片时,系统会启动延迟提示机制。在图片开始处理后 3 秒,如果处理尚未完成,系统会发送一条随机的"正在处理"提示消息,提升用户等待体验。提示文本按图片数量分桶(single/few/batch)并支持 visual 和 fun 两种风格,按 7:3 权重随机选择。
图片消息进入 dispatch_image 调度后,系统会:
- 1. 注册图片处理中的状态(
mark_image_in_flight) - 2. 下载微信图片素材(
kf/get_media) - 3. 运行图片处理流水线
- 4. 将图片描述注入 Agent 上下文
- 5. 完成后清除处理状态
Sources: routers/wechat.py, routers/wechat.py
微信小程序通道
小程序通道提供了比客服通道更丰富的交互能力。它包含 HTTP REST API 和 WebSocket 两种接入方式,支持流式对话和实时事件推送。
HTTP API 接口
小程序 HTTP API 以 /api/wechat/ 为前缀,需要 JWT Token 认证(通过 LoginRequired 依赖注入)。主要端点包括:
| 端点 | 方法 | 功能 |
|---|---|---|
/api/wechat/chat | POST | 发送聊天消息(支持 text + image 混合) |
/api/wechat/chat/upload-image | POST | 上传聊天图片(最大 50MB) |
/api/wechat/ws | WebSocket | 实时聊天与事件推送 |
/api/wechat/user/login | POST | 小程序登录(code 换 token) |
/api/wechat/user/profile | GET/PUT | 用户资料读写 |
聊天端点 POST /api/wechat/chat 接受 MiniappChatRequest,支持两种消息格式:
- - legacy 格式:
message字段传递纯文本 - - multipart 格式:
messages数组支持 text 和 image 混合输入
请求处理流程:构建入站消息部件 → 内容审核 → 访问控制 → Agent 执行 → 返回回复。
Sources: routers/miniapp/chat.py, routers/miniapp/chat.py
WebSocket 实时通道
WebSocket 端点 WS /api/wechat/ws 提供双向实时通信能力。连接建立时通过 resolve_ws_user 验证 Token(支持 query param 和 Authorization header 两种方式),注册到连接池 miniapp_ws.py。
WebSocket 通道的核心能力包括:
- 1. 流式对话:消息通过 WebSocket 发送,Agent 回复逐 token 推送
- 2. 穿搭结果推送:后台生成完成后,通过
send_miniapp_event实时推送穿搭卡片 - 3. 状态同步:支持 outfit 轮询模式——客户端发送 outfit 请求后,服务端以 0.8 秒间隔轮询数据库状态,完成后立即推送
连接管理通过 _user_connections 字典维护(user_id → set[WebSocket]),支持同一用户多个连接。发送失败的连接自动清理。
flowchart LR
subgraph Client["小程序客户端"]
ChatUI["聊天界面"]
end
subgraph Server["服务端"]
WS["WS 端点"]
ConnPool["连接池<br/>miniapp_ws"]
Agent["Agent 运行时"]
OutfitPoll["Outfit 轮询器"]
end
ChatUI <-->|"text / image"| WS
WS <--> ConnPool
WS --> Agent
Agent -->|"reply chunk"| WS
OutfitPoll -->|"outfit card push"| ConnPool
ConnPool -->|"event"| ChatUI
Sources: routers/miniapp/ws.py, messaging/miniapp_ws.py, routers/miniapp/ws_common.py
小程序登录与用户身份
小程序用户通过 code 换取 JWT Token 完成登录。user.py 中的 /api/wechat/user/login 端点接收微信 login 接口返回的 code,调用微信 jscode2session API 获取 openid 和 session_key,然后生成应用层 JWT Token。
Token 验证通过 get_token_data 函数完成,支持开发环境的 DEV_TOKEN 直通模式。用户身份最终映射为数据库中的 user_id(整数主键),与客服通道的 external_userid 通过 unionid 关联。
Sources: routers/miniapp/user.py, routers/miniapp/ws_common.py
统一消息调度框架
两条通道的差异在入口层处理后,汇聚到 app/messaging/ 下的统一调度框架。这一框架的核心是 session_dispatcher.py 中的会话状态机,它保证每个会话同时只有一个 Agent 调用在运行。
会话调度器
调度器管理三类并发状态:
| 状态类型 | 存储结构 | 语义 |
|---|---|---|
| 文本任务 | _text_tasks[session_id] | 当前运行的 Agent 文本处理 Task |
| 图片处理 | _image_processing[session_id] | 图片处理中的 Event 信号 |
| 队列排空 | _queue_draining[session_id] | 防止 drain_queue 重入 |
调度规则如下:
- - 图片阻塞:图片处理期间,后续所有消息排队等待
- - 文本合并:新文本到达时,可取消正在等待的旧文本任务并合并内容(防抖行为)
- - 自动排空:任何任务完成后自动排空队列
渠道适配器只需提供两个回调:on_text(处理文本消息)和 prepare_image(解析图片引用)。客服通道通过 _make_wechat_handlers 生成绑定 open_kfid 和 external_userid 的闭包;小程序通道直接在路由中调用 Agent 服务。
Sources: messaging/session_dispatcher.py, messaging/session_dispatcher.py
入站消息归一化
inbound.py 将不同渠道的入站消息统一为 InboundMessagePart 列表,支持 text 和 image 两种部件类型。图片引用通过 ImageInputRef 数据类标准化,支持三种来源:
| 来源类型 | 说明 | 典型场景 |
|---|---|---|
wechat_media_id | 微信临时素材 ID | 客服通道图片 |
stored_image_id | 数据库图片 ID | 小程序已上传图片 |
remote_url | 远程图片 URL | 第三方图片 |
Sources: messaging/inbound.py, messaging/inbound.py
Agent 执行服务
agent_service.py 提供 run_inbound_agent_turn 函数,封装了完整的 Agent 执行流程:创建会话 → 保存用户消息 → 执行 Agent → 保存助手回复 → 标记回复状态。两条通道都通过此函数驱动 Agent,确保消息持久化和状态管理的一致性。
Sources: messaging/agent_service.py, messaging/agent_service.py
渠道专属配置
系统通过 data/channels/ 目录为不同渠道提供专属的提示词和技能配置。每个渠道目录下可包含:
| 目录/文件 | 用途 |
|---|---|
agents/ | 渠道专属 Agent 提示词 |
skills/ | 渠道专属技能配置 |
outfit_summary.*.txt | 渠道专属穿搭摘要模板 |
当前支持的渠道目录:
- -
wechat_kefu— 企业微信客服 - -
wechat_miniapp— 微信小程序
channel_prompts.py 提供了 load_optional_channel_prompt 函数,按 channels/{channel_name}/{relative_path} 路径加载渠道专属内容。如果文件不存在则返回空字符串,实现优雅降级。
Sources: channel_prompts.py
环境变量配置
微信集成相关的环境变量如下:
| 变量名 | 必填 | 说明 |
|---|---|---|
WECHAT_TOKEN | 客服通道必须 | 企业微信回调 Token |
WECHAT_ENCODING_AES_KEY | 客服通道必须 | 消息加解密 AES Key |
WECHAT_CORP_ID | 客服通道必须 | 企业微信 Corp ID |
WECHAT_SECRET | 客服通道必须 | 企业微信应用 Secret |
WECHAT_MINIPROGRAM_APPID | 小程序卡片必须 | 小程序 AppID |
WECHAT_MINIPROGRAM_TITLE | 否 | 小程序卡片标题(默认"点击打开小程序") |
WECHAT_MINIPROGRAM_PAGEPATH | 否 | 小程序卡片页面路径(默认 pages/index/index) |
WECHAT_MINIPROGRAM_IMAGE_URL | 否 | 缩略图 OSS URL(自动上传刷新 media_id) |
MESSAGE_ACCESS_CONTROL_ENABLED | 否 | 消息准入控制开关(默认 true) |
客服通道要求四个 WECHAT_* 变量全部配置,否则返回 503。小程序通道仅需 JWT 认证,不依赖客服相关配置。
Sources: .env.example, config.py
小程序路由映射
miniprogram_routes.py 定义了系统支持的小程序页面路由映射,用于将 Agent 输出中的 mp:// 协议链接转换为小程序原生页面跳转:
| 路由键 | 小程序页面路径 | 说明 |
|---|---|---|
generate_history | pages/history-images/history-images.html | 生成历史 |
outfit_detail | pages/chat/chat.html | 穿搭详情(重写为当前聊天) |
my_closet | pages/closet/closet.html | 我的衣橱 |
edit_profile | pages/info-collect/info-collect.html | 编辑资料 |
在客服通道中,这些链接被转换为 Markdown 菜单项;在小程序通道中,被转换为纯文本或 WebSocket 选项。
Sources: wechat/miniprogram_routes.py, wechat/miniprogram_routes.py
内容安全审核
两条通道的消息都经过阿里云内容安全审核,但策略有所不同:
| 审核维度 | 客服通道 | 小程序通道 |
|---|---|---|
| 文本审核 | 启用 | 启用 |
| 图片审核 | 启用 | 启用 |
| 审核渠道过滤 | 默认不审核(配置可开启) | 默认审核 |
| 置信度阈值 | 95% | 95% |
config.yml 中 aliyun_content_audit_enabled_channels 默认仅包含 wechat_miniapp,即客服通道默认跳过内容审核。这是基于客服通道误报率较高的生产实践调优。
Sources: config.yml, routers/wechat.py
消息准入控制
系统提供统一的消息准入控制机制,通过 message_access_control.py 实现。准入检查在两条通道的消息处理入口处执行,支持白名单、黑名单和授权码三种模式。
准入检查的参数包括渠道标识、服务账号(客服通道为 open_kfid)、用户身份(user_id + openid + unionid)。被拒绝的用户会收到国际化错误提示。
Sources: routers/wechat.py, routers/miniapp/chat.py
调试与运维
客服通道关键日志标签:
- -
[wechat_api:start/done/error]— 企业微信 API 调用耗时与结果 - -
[wechat:sync]— 消息同步状态 - -
[wechat:greeting]— 欢迎语发送
小程序通道关键日志标签:
- -
[miniapp-ws]— WebSocket 连接生命周期 - -
[miniapp-chat]— HTTP 聊天请求
Redis 键清单(客服通道):
| 键模式 | TTL | 用途 |
|---|---|---|
wechat:kefu:access_token | 7000s | access_token 缓存 |
wechat:kefu:cursor | 持久 | 消息同步游标 |
wechat:kefu:msg:{msgid} | 24h | 消息去重 |
wechat:kefu:known_user:{extid} | 7d | 欢迎语去重 |
wechat:kefu:quota:{extid} | 48h | 消息配额计数 |
wechat:kefu:thumb_media_id | 2.5d | 缩略图素材缓存 |
<a id="17-hui-hua-fen-fa-yu-hui-fu-zhuang-tai"></a>
---
会话分发与回复状态
- - Section: 核心模块详解
- - Group: 消息处理与渠道
- - Source File:
17-hui-hua-fen-fa-yu-hui-fu-zhuang-tai.md
当用户在短时间内连续发送多条消息(文本+图片、多条文本等),系统必须保证 Agent 按顺序处理且不产生交错回复。本页详细解析 会话分发器(Session Dispatcher) 如何管理并发消息的调度,以及 回复状态(Reply State) 如何在 Redis 中追踪每条消息的回复进度,防止重复回复和状态漂移。
分发状态机概览
整个会话分发机制以模块级字典维护状态,不依赖数据库锁,仅在 asyncio 事件循环中被安全修改。下图展示了一条消息从入站到 Agent 处理完成的完整生命周期:
stateDiagram-v2
[*] --> Incoming: 用户发送消息
Incoming --> CheckQueue: dispatch_text / dispatch_image
state CheckQueue {
direction LR
[*] --> ImageInFlight: 图片正在处理?
[*] --> TextRunning: 文本任务正在运行?
[*] --> QueueNotEmpty: 队列非空?
[*] --> Ready: 均否
}
ImageInFlight --> Enqueued: 入队等待
TextRunning --> ArbiterCheck: 是否有仲裁器?
QueueNotEmpty --> Enqueued: 入队等待
ArbiterCheck --> MergeAndCancel: interrupt / fresh
ArbiterCheck --> ParallelEnqueue: parallel
ArbiterCheck --> DropMessage: handled
Ready --> ScheduleTask: 调度文本任务
MergeAndCancel --> ScheduleTask: 合并内容后调度
ScheduleTask --> DebounceSleep: 300ms 去抖
DebounceSleep --> ExecuteAgent: 调用 on_text
ExecuteAgent --> DrainQueue: 完成后自动排空队列
DrainQueue --> [*]: 下一条消息
Sources: session_dispatcher.py
核心状态字典
会话分发器使用四个模块级字典追踪每个 session_id 的并发状态。这些字典仅在 asyncio 事件循环中被修改,无需锁机制——前提是部署模型为单事件循环的 FastAPI 进程。
| 字典名 | 类型 | 用途 |
|---|---|---|
_text_tasks | dict[str, asyncio.Task] | 当前正在运行的文本 Agent 任务 |
_text_task_contents | dict[str, str] | 正在运行任务的原始内容(用于后续合并) |
_image_processing | dict[str, asyncio.Event] | 图片处理中的阻塞信号 |
_queue_draining | set[str] | 正在排空队列的 session(重入保护) |
当一个 Agent 任务完成后,调度器在 finally 块中自动调用 drain_queue 来处理队列中下一条消息,形成链式排空。
Sources: session_dispatcher.py
消息分发入口函数
调度器对外暴露三个入口函数,渠道路由(如微信客服、小程序)通过它们驱动状态机,从不直接操作内部字典。
dispatch_text
文本消息的分发入口。其核心逻辑分为三条路径:
- 1. 阻塞路径:若图片正在处理或队列非空,消息直接入队等待
- 2. 仲裁路径:若有正在运行的文本任务,通过
FollowupArbiter决策后行动 - 3. 直接调度路径:无阻塞、无运行任务时,立即创建新的文本任务
flowchart TD
A[dispatch_text] --> B{图片处理中<br>或队列非空?}
B -->|是| C[enqueue → 返回]
B -->|否| D{有运行中的<br>文本任务?}
D -->|否| H[_schedule_text_task]
D -->|是| E[调用 arbiter]
E --> F{arbiter 决策}
F -->|interrupt| G[合并内容,<br>取消旧任务]
F -->|fresh| G2[丢弃旧内容,<br>取消旧任务]
F -->|parallel| I[消息入队,<br>不中断旧任务]
F -->|handled| J[旧任务保持,<br>新消息丢弃]
G --> H
G2 --> H
关键设计:文本任务创建后会先经历 300ms 去抖睡眠(asyncio.sleep(0.3)),在此窗口内到达的新消息可通过仲裁器合并或取消,避免用户快速输入时产生多次 Agent 调用。
Sources: session_dispatcher.py
dispatch_image
图片消息的分发入口。图片具有最高优先级——若当前有文本任务正在运行,图片到达后会取消该文本任务(保留其内容用于后续合并),然后立即启动图片处理流程。
图片处理完成后,调度器会将累积的图片描述和后续文本合并为一条消息,交给正常的可取消文本任务路径处理。这意味着图片处理期间用户发送的文本不会丢失。
Sources: session_dispatcher.py
drain_queue
队列排空函数是整个调度器的"心脏"。它在每次任务完成后被自动调用,从数据库中弹出待处理消息并按类型分发:
- - 若弹出消息中包含图片:标记图片处理中,启动
_run_accumulated_image_batch后台任务 - - 若弹出消息全是文本:合并多条文本内容后调度新的文本任务
- - 若队列为空或有活跃任务:终止排空循环
重入保护机制通过 _queue_draining 集合实现——同一个 session 的重复排空调用会被静默丢弃。
Sources: session_dispatcher.py
后续消息仲裁(Followup Arbiter)
当用户在 Agent 任务运行期间发送新消息时,调度器需要决定如何处理。系统通过 FollowupArbiter 协议支持可插拔的决策策略,微信客服渠道使用基于 LLM 的意图仲裁器来自动分类:
| 决策动作 | 行为 | 场景示例 |
|---|---|---|
interrupt | 取消旧任务,合并内容后重新调度 | 用户补充了新信息 |
fresh | 取消旧任务,丢弃旧内容,仅处理新消息 | 用户改变主意要重新开始 |
parallel | 旧任务继续运行,新消息入队等待 | 用户发送了无关的附带消息 |
handled | 旧任务继续,新消息直接丢弃 | 新消息已被旧任务覆盖 |
仲裁器构建上下文快照时会并发加载近期对话历史、用户记忆和用户画像,为 LLM 决策提供充足信息。
Sources: wechat_intent_arbitration.py, session_dispatcher.py
回复状态追踪
回复状态模块在 Redis 中维护每条用户消息的回复进度,解决两个核心问题:快速回复(Fast Response)的准确性和消息级回复去重。
Redis 键结构
回复状态使用五类 Redis 键,统一使用 7 天 TTL:
| 键前缀 | 存储类型 | 用途 |
|---|---|---|
agent:reply_state:pending:{session_id} | SET | 当前 session 待回复的消息键集合 |
agent:reply_state:message:{message_key} | STRING (JSON) | 单条消息的回复状态(是否已回复、阶段等) |
agent:reply_state:latest:{session_id} | STRING (JSON) | session 最新用户消息 ID |
agent:reply_state:counter:{message_id} | STRING (JSON) | 最新消息的回复计数 |
agent:reply_state:pending_seed:{session_id} | STRING | 跨消息传递的回复计数种子 |
Sources: reply_state.py
回复状态生命周期
一条用户消息从到达到回复完成的状态变化流程如下:
sequenceDiagram
participant Channel as 渠道路由
participant RS as Reply State (Redis)
participant Dispatcher as Session Dispatcher
participant Agent as Agent Service
Channel->>RS: register_pending_reply_keys(session, keys)
Channel->>RS: register_latest_user_message(session, msg_id)
Note over RS: counter:{msg_id}.reply_count = 0
Dispatcher->>Agent: on_text(session, content, ...)
Agent->>Agent: run_agent_with_memory(...)
alt 快速回复可用
Agent->>RS: can_send_fast_response(session, msg_id)
RS-->>Agent: true (reply_count == 0)
Agent->>Channel: 发送快速回复
end
Agent->>Channel: 发送最终回复
Agent->>RS: mark_session_reply_sent(session, phase="final")
Note over RS: counter:{msg_id}.reply_count += 1<br>pending keys 清空<br>message:{key}.replied = true
Sources: reply_state.py, agent_service.py
快速回复守卫
can_send_fast_response 函数通过比较消息 ID 和回复计数来判断是否允许发送快速回复。条件为:消息 ID 匹配且 reply_count 为 0。一旦 mark_session_reply_sent 被调用(回复计数 +1),快速回复就被自动禁用——这确保了快速回复只在 Agent 尚未输出最终回复时生效。
Sources: reply_state.py, fast_response.py
回复种子机制
register_pending_latest_reply_seed 提供了一种跨消息传递回复计数的机制。当渠道(如微信客服)在用户消息到达前就已发送了一条通知回复(如"图片处理中…"),它可以通过 seed 预先累加回复计数。当 register_latest_user_message 被调用时,seed 值会被继承到新消息的 counter 中,确保 can_send_fast_response 对该消息返回 false。
Sources: reply_state.py
分发任务守卫(Dispatch Guard)
dispatch_guard 模块使用 Python 的 ContextVar 实现协程级别的任务身份验证,防止被取消的过期任务向用户发送回复。
每当新文本任务被创建时,调度器会生成一个唯一 token 并绑定到 ContextVar。任务内部的关键节点(如 Agent 调用前后)通过 is_current_dispatch_task 检查自身是否仍为当前活跃任务。若 token 已被更新(说明新消息已到达并替换了该任务),过期任务将静默中止,不会执行后续回复。
flowchart LR
A[新消息到达] --> B[生成 task_token = uuid4]
B --> C[set_current_dispatch_token]
C --> D[创建 asyncio.Task]
D --> E{is_current_dispatch_task?}
E -->|是| F[继续执行 on_text]
E -->|否| G[静默中止]
F --> H[发送回复]
H --> I[clear_current_dispatch_token]
Sources: dispatch_guard.py, session_dispatcher.py
渠道集成模式
各渠道路由通过实现回调协议来接入分发器。以微信客服为例,它需要提供四个回调:
| 回调 | 协议类型 | 职责 |
|---|---|---|
on_text | TextHandler | 执行 Agent 调用并发送回复 |
prepare_image | ImagePreprocessor | 将渠道图片引用解析为 ImagePipelineResult |
on_image_error | ImageErrorHandler | 图片预处理失败时发送错误提示 |
arbiter | FollowupArbiter | 对并发消息进行意图分类决策 |
入站消息经过访问控制、内容审核、快捷方式匹配等前置处理后,才进入 dispatch_text / dispatch_image。消息的 reply_state_key(如微信 msgid)在入口处即被注册到 Redis,确保后续的回复状态追踪覆盖完整生命周期。
图片批处理流程
当图片到达时,调度器不会立即调用 Agent——而是启动 _run_accumulated_image_batch 后台任务。该任务会持续排空队列,预处理所有排队的图片(上传 OSS、去重检测、生成描述),然后将合并后的图片描述 + 累积文本一起交给可取消的文本任务路径。
这种设计确保了:用户连续发送 5 张图片时,系统会等待所有图片处理完成后再调用 Agent,而非逐张处理产生 5 次独立对话。
Sources: session_dispatcher.py
并发安全总结
整个会话分发与回复状态系统通过三层机制保证并发安全:
| 层级 | 机制 | 保护对象 |
|---|---|---|
| 事件循环级 | 模块级字典仅在 asyncio 单线程中修改 | _text_tasks 等状态 |
| 协程级 | ContextVar + 唯一 token | 防止过期任务发送回复 |
| 分布式级 | Redis 原子操作(SET/SADD/INCR) | 回复状态跨进程一致 |
这三层机制共同保证了:即使在高频消息场景下(用户快速连发、图片+文本混合),系统也绝不会出现交错回复或重复回复。
<a id="18-mem0-ji-yi-guan-li"></a>
---
Mem0 记忆管理
- - Section: 核心模块详解
- - Group: 记忆与洞察
- - Source File:
18-mem0-ji-yi-guan-li.md
本页介绍 outfit_agent 的长期用户记忆系统——基于 Mem0 框架构建的语义记忆层。该系统使 Agent 能够跨会话积累用户偏好(体型特征、风格倾向、预算区间、场景禁忌等),并在每次对话前自动注入相关记忆上下文,从而实现真正的"越用越懂你"个性化穿搭服务。
记忆系统由两条并行链路驱动:在线实时提取(每轮对话后可选执行)和离线每日洞察分析(定时批任务,基于穿搭记录、衣橱单品与聊天历史的多维度深度分析)。两条链路最终都将结构化事实写入同一个 Mem0 存储,由 Agent 在对话发起时统一检索。
系统架构总览
Mem0 记忆系统的核心设计可概括为 "先读后写、双链并行、单一存储"。下图展示了从用户消息到达至记忆写入的完整数据流:
flowchart TB
subgraph Online["在线链路(每轮对话)"]
U[用户消息] --> S[search_user_memory<br/>语义检索相关记忆]
S --> |格式化记忆上下文| A[Agent 推理]
A --> R[生成回复]
R --> |开启时| W[add_conversation_memory<br/>实时提取事实]
end
subgraph Offline["离线链路(每日定时)"]
CRON[每日 00:00 调度] --> SEL[InsightSelector<br/>扫描候选用户]
SEL --> COL[InsightCollector<br/>收集三维增量数据]
COL --> ANA[InsightAnalyzers<br/>分维度 LLM 分析]
ANA --> MERGE[analyze_merge<br/>冲突仲裁引擎]
MERGE --> SYNC[Mem0Synchronizer<br/>执行 add/update/delete]
end
subgraph Storage["统一存储"]
W --> PG[(pgvector<br/>outfit_mem0_memories)]
SYNC --> PG
S --> PG
end
subgraph Admin["管理接口"]
PG --> API[Admin API<br/>查看/编辑/删除记忆]
PG --> DASH[Dashboard<br/>记忆浏览面板]
end
style Online fill:#e8f5e9,stroke:#2e7d32
style Offline fill:#e3f2fd,stroke:#1565c0
style Storage fill:#fff3e0,stroke:#e65100
style Admin fill:#f3e5f5,stroke:#6a1b9a
两条链路的核心差异:
| 维度 | 在线链路 | 离线链路 |
|---|---|---|
| 触发时机 | 每轮对话完成后(可选) | 每日零点定时任务 |
| 数据来源 | 当前单轮 user+assistant 消息对 | 穿搭记录、衣橱单品、历史聊天 |
| 提取方式 | Mem0 内置 LLM 自动提取事实句 | 多维度专属 Prompt 分析 → 冲突仲裁 |
| 配置开关 | mem0_realtime_analysis_enabled | insight_scheduler_cron + 触发阈值 |
| 推荐策略 | 默认关闭,依赖离线洞察 | 主力记忆来源,覆盖更全面 |
Sources: app/memory/__init__.py, app/memory/ops.py, app/insights/service.py
初始化与配置
启动流程
Mem0 以 AsyncMemory 单例 模式运行,在 FastAPI 应用启动阶段通过 init_memory() 一次性初始化。该初始化嵌入在 AgentOS 的生命周期管理中,由 app/main.py 的 _mem0_lifespan 控制:
sequenceDiagram
participant Main as app/main.py
participant Lifespan as _mem0_lifespan
participant Client as memory/client.py
participant Mem0 as AsyncMemory
participant PG as pgvector (PostgreSQL)
Main->>Lifespan: FastAPI lifespan 启动
Lifespan->>Lifespan: DB schema 迁移
Lifespan->>Client: init_memory()
Client->>Client: 解析 database_url → pgvector 参数
Client->>Client: 构建 MemoryConfig
Client->>Mem0: AsyncMemory(config)
Mem0->>PG: 连接并验证 collection
Client-->>Lifespan: 单例就绪
Lifespan-->>Main: 启动完成
init_memory() 内部完成三项关键配置组装:
- 1. 向量存储(pgvector):使用项目现有的 PostgreSQL 数据库,collection 名称默认为
outfit_mem0_memories,embedding 维度与text_embedding模型配置一致(默认 1024)。 - 2. LLM 提取器:使用
flash模型(默认 temperature=0.1)驱动记忆事实提取。 - 3. Embedding 模型:使用
text_embedding模型生成语义向量,通过 OpenAI 兼容接口调用。
Sources: app/memory/client.py, app/main.py
配置参数
所有记忆相关配置通过环境变量或 config.yml 控制,由 app/config.py 的 Settings 类统一管理:
| 配置项 | 环境变量 | 默认值 | 说明 |
|---|---|---|---|
mem0_enabled | MEM0_ENABLED | true | 是否启用 Mem0 记忆系统 |
mem0_realtime_analysis_enabled | MEM0_REALTIME_ANALYSIS_ENABLED | false | 是否对每轮对话实时提取记忆 |
mem0_collection_name | MEM0_COLLECTION_NAME | outfit_mem0_memories | pgvector 集合名称 |
mem0_search_limit | MEM0_SEARCH_LIMIT | 10 | 单次语义检索最大返回条数 |
mem0_history_db_path | MEM0_HISTORY_DB_PATH | data/mem0_history.db | Mem0 内部记忆变更历史 SQLite 路径 |
离线洞察分析的额外配置:
| 配置项 | 默认值 | 说明 |
|---|---|---|
insight_outfit_delta_threshold | 20 | 触发穿搭洞察的新增穿搭数阈值 |
insight_item_delta_threshold | 20 | 触发衣橱洞察的新增单品数阈值 |
insight_chat_interval_days | 30 | 聊天洞察的间隔天数 |
insight_merge_history_days | 730 | 仲裁时穿搭/聊天历史窗口(天) |
insight_scheduler_cron | 0 0 * | 每日调度 cron 表达式 |
insight_scheduler_timezone | Asia/Shanghai | 调度时区 |
insight_batch_size | 200 | 每批扫描用户数 |
insight_user_concurrency | 5 | 并发处理用户数 |
Sources: app/config.py, .env.example
在线记忆操作
在线链路是 Agent 每次响应用户消息时的记忆读写路径,由 app/memory/ops.py 封装为三个核心操作。
记忆检索(Read-Before)
search_user_memory() 是 Agent 运行前的记忆注入入口。它执行 双重语义搜索:先搜全局用户画像记忆(agent_id 维度),再搜当前会话上下文记忆(run_id 维度),合并去重后格式化为结构化文本块注入 Agent 输入。
flowchart LR
Q[用户消息] --> Search[search_user_memory]
Search --> |query + user_id| P[Profile 记忆<br/>agent_id 维度]
Search --> |query + user_id + session_id| S[Session 记忆<br/>run_id 维度]
P --> Merge[合并去重<br/>按 memory id]
S --> Merge
Merge --> Format[格式化输出]
Format --> |---用户记忆---<br/>[用户画像]<br/>- 记忆1<br/>- 记忆2<br/>[本次会话上下文]<br/>- 记忆3<br/>--------------| Inject[注入 agent_input]
格式化后的记忆文本按 [用户画像] 和 [本次会话上下文] 两个区块组织,直接拼接到用户消息前面,使 Agent 在推理时能自动参考历史偏好。
在 app/agent.py 的 run_agent_with_memory() 中,检索延迟和记忆字符数会被记录用于可观测性监控:
[agent:memory] user=<id> session=<id> elapsed_ms=<ms> memory_chars=<len>
Sources: app/memory/ops.py, app/agent.py
记忆写入(Write-After)
add_conversation_memory() 在 Agent 完成回复后异步执行。它将当前轮的 user+assistant 消息对提交给 Mem0,由 Mem0 内部的 LLM 根据 mem_extraction.zh.txt 中定义的提取规则自动识别并存储事实。
写入操作通过 asyncio.Semaphore(2) 限制并发数,避免 Mem0 内部同步 psycopg2 连接池被过多异步任务压垮。所有写入异常均被捕获并记录为 warning,确保记忆故障不会阻断主响应路径。
实时提取默认关闭(mem0_realtime_analysis_enabled=false),推荐依赖离线洞察获得更全面、经过冲突仲裁的记忆质量。
提取提示词定义了 5 大提取维度:
| 维度 | 提取内容 | 示例 |
|---|---|---|
| 生理与视觉特征 | 身高、体重、体型、肤色、发型、性别 | "用户身高168cm,体型偏沙漏型" |
| 风格偏好 | 整体风格、喜欢/不喜欢的单品、品牌偏好 | "用户偏好法式优雅和简约通勤风格" |
| 场景偏好 | 职业背景、常见场合、文化禁忌 | "用户需要职场通勤和约会两种场景穿搭" |
| 预算信息 | 常规预算区间、特定场景预算 | "用户单品常规预算在500-1000元之间" |
| 衣橱与历史 | 已有单品描述、衣橱风格摘要、历史偏好 | "用户衣橱以黑白灰基础款为主" |
Sources: app/memory/ops.py, data/prompts/memory/mem_extraction.zh.txt, app/agent.py
用户风格画像查询
get_user_style_profile() 提供面向 Agent 工具调用的风格画像快捷接口。它使用固定的风格相关查询词进行语义搜索,只返回与穿搭决策相关的记忆子集,而非全量记忆:
"用户风格偏好 体型特征 预算区间 着装禁忌 品牌偏好"
该函数与 get_user_profile()(返回全量记忆列表)形成互补——前者用于 Agent prompt 注入,后者用于管理面板展示。
Sources: app/memory/ops.py, app/memory/ops.py
离线洞察分析链路
离线链路是记忆系统的核心深度引擎,由 app/insights/ 模块实现。它通过每日定时任务扫描活跃用户,从穿搭记录、衣橱单品和聊天历史三个维度提取深层洞察,经冲突仲裁后同步到 Mem0。
整体流程
flowchart TB
START[每日 00:00<br/>scheduler 触发] --> SCAN[InsightSelector<br/>扫描全部用户]
SCAN --> TRIGGER{是否满足<br/>触发条件?}
TRIGGER --> |否| SKIP[跳过]
TRIGGER --> |是| COLLECT[InsightCollector<br/>收集增量数据]
COLLECT --> O{新增穿搭<br/>>= 阈值?}
COLLECT --> I{新增单品<br/>>= 阈值?}
COLLECT --> C{距上次聊天洞察<br/>>= 30天?}
O --> |是| AO[analyze_outfit<br/>穿搭维度分析]
I --> |是| AI[analyze_item<br/>衣橱维度分析]
C --> |是| AC[analyze_chat<br/>对话维度分析]
C --> |是| AT[analyze_tone<br/>语调风格分析]
AO --> SAVE1[写入 user_insights]
AI --> SAVE1
AC --> SAVE1
AT --> SAVE1
SAVE1 --> AGG[聚合数据]
AGG --> |穿搭: 近2年洞察<br/>衣橱: 最新一次<br/>对话: 近2年洞察<br/>+ 当前 Mem0 记忆| MERGE[analyze_merge<br/>冲突仲裁引擎]
MERGE --> SYNC[Mem0Synchronizer<br/>apply_actions]
SYNC --> |add/update/delete| MEM0[(Mem0<br/>pgvector)]
style START fill:#e8f5e9,stroke:#2e7d32
style MERGE fill:#fff3e0,stroke:#e65100
style MEM0 fill:#e3f2fd,stroke:#1565c0
触发条件
InsightSelector 对每个用户检查三条触发路径,满足任意一条即纳入当晚洞察批次:
- 1. 穿搭增量:距上次穿搭洞察后,新增穿搭记录数 >=
insight_outfit_delta_threshold(默认 20) - 2. 衣橱增量:距上次衣橱洞察后,新增
source in (wardrobe, wardrobe_want)且status != disposed的单品数 >=insight_item_delta_threshold(默认 20) - 3. 聊天到期:距上次聊天洞察 >=
insight_chat_interval_days(默认 30 天)
首次洞察的特殊处理:若用户从未执行过洞察,只要累计穿搭数或衣橱单品数达到阈值,或近 30 天有聊天记录,均可触发首轮基线分析。
Sources: app/insights/selectors.py, app/insights/config.py
三维分析器
InsightAnalyzers 封装了四类 LLM 分析器,每个分析器使用专属 Prompt 模板处理对应维度数据:
| 分析器 | 输入数据 | 输出格式 | Prompt 模板 |
|---|---|---|---|
analyze_outfit | 近期穿搭记录(含行为信号) | <csv> 维度,置信度,观察 | insight_outfit.zh.txt |
analyze_item | 全量衣橱单品 | <csv> 维度,置信度,观察 | insight_item.zh.txt |
analyze_chat | 增量聊天记录 | <csv> 维度,置信度,观察 | insight_chat.zh.txt |
analyze_merge | 三维洞察 + 当前 Mem0 | <yaml> actions 列表 | insight_merge.zh.txt |
聊天分析器提取 9 个核心维度:
| 维度 | 含义 | 示例 |
|---|---|---|
flatter_areas | 需要修饰/遮挡的部位 | "大臂粗,需要遮挡" |
highlight_areas | 满意/想要展露的部位 | "锁骨好看,想展示" |
personal_taboos | 绝对拒绝的元素 | "绝不穿高饱和色" |
cultural_taboos | 场合/信仰禁忌 | "去中东出差的着装限制" |
Aesthetic_Targets | 向往的风格/氛围 | "想走老钱风" |
Signature_Pieces | 极度热爱的单品 | "就是喜欢买短裙" |
Blind_Spot | 想尝试的新品类 | "想试一下辣妹装" |
Brand_Affinity | 品牌赞美或吐槽 | "最近觉得 Miu Miu 太少女" |
Price_Range | 品类心理价位 | "T恤超500就不买" |
每个观察事件都附带 置信度等级(High/Medium/Low),由 LLM 根据用户用词强度自动评估,作为后续仲裁权重参考。
Sources: app/insights/analyzers.py, data/prompts/memory/insight_chat.zh.txt
冲突仲裁引擎
冲突仲裁是离线链路中最关键的一步。analyze_merge 将三维洞察输出与用户当前 Mem0 记忆一同送入仲裁 Prompt,由 LLM 执行 7 大冲突法则,输出标准化的 add/update/delete 动作列表:
| 法则 | 冲突场景 | 仲裁策略 |
|---|---|---|
| 身体密码 | 历史标记某部位为高亮,新对话抱怨需遮挡 | 最新对话无条件覆盖 |
| 审美向往 | 行为保存 A 风格,语言追求 B 风格 | 80/20 混合策略 |
| 绝对雷区 | 衣橱有某类单品,但最新对话明确拉黑 | 一票否决,写入 P0 级雷区 |
| 预算波动 | 历史高消费但当前表露低预算 | 以最新会话为准 |
| 品牌倾向 | 嘴说喜欢但无购买支撑 / 衣橱多但今天拉黑 | 观察期标签 / 静默折旧 |
| 执念单品 | 高频穿戴单品表示"看腻了" | 冷却清单(冻结 30 天) |
| 盲区单品 | 从未尝试的品类突然要求试 | 破壁安全网,挂载 Body_Code 防御 |
Mem0Synchronizer 负责解析仲裁输出并执行实际的 Mem0 操作:
flowchart LR
RAW[LLM 仲裁输出<br/>yaml 标签] --> PARSE[parse_actions<br/>YAML 解析校验]
PARSE --> VALID{校验通过?}
VALID --> |否| ERR[抛出 ValueError]
VALID --> |是| LOAD[加载当前记忆]
LOAD --> CHECK{memory_id<br/>归属校验}
CHECK --> |通过| EXEC[执行动作]
EXEC --> ADD[mem.add]
EXEC --> UPD[mem.update]
EXEC --> DEL[mem.delete]
CHECK --> |不归属| ERR2[抛出 ValueError]
Sources: data/prompts/memory/insight_merge.zh.txt, app/insights/mem0_sync.py
记忆分类与管理
自动分类系统
app/memory/admin.py 实现了基于关键词匹配的记忆分类机制,将每条记忆归入 7 个类别之一,用于 Dashboard 展示和管理筛选:
| 分类代码 | 显示标签 | 关键词示例 |
|---|---|---|
body_profile | 身材特征 | 身高、体重、梨形、苹果型、沙漏、肤色 |
style_preference | 风格偏好 | 风格、偏好、简约、休闲、法式、韩系 |
occasion_scene | 场景需求 | 约会、上班、通勤、旅行、婚礼 |
budget_taboo | 预算与禁忌 | 预算、元、不喜欢、禁忌、避免 |
location | 地域信息 | 上海、北京、深圳、城市 |
wardrobe_history | 衣橱与历史 | 衣橱、历史、单品、品牌名 |
other | 其他 | 不匹配以上任何规则 |
分类逻辑通过 classify_memory() 函数实现,对记忆文本进行小写归一化后逐一匹配关键词列表。other 类别的 SQL 查询策略是排除所有已知关键词,即"不属于任何已知类别"。
Sources: app/memory/admin.py
Admin API
管理员可通过 REST API 管理 Mem0 记忆,所有接口位于 /api/admin/memories 路径下,需要 Bearer Token 认证:
| 方法 | 路径 | 功能 |
|---|---|---|
GET | /api/admin/memories | 分页列出记忆(支持按 user_id、run_id、category、关键字筛选) |
GET | /api/admin/memories/summary | 获取记忆摘要统计(总数、用户数、分类分布) |
GET | /api/admin/memories/{id} | 获取单条记忆详情 |
GET | /api/admin/memories/{id}/history | 获取单条记忆变更历史 |
PATCH | /api/admin/memories/{id} | 更新记忆内容 |
DELETE | /api/admin/memories/{id} | 删除记忆 |
列表接口直接查询 pgvector 的 outfit_mem0_memories 表,通过 payload #>> '{}' 的 JSON 路径表达式提取字段,支持基于 ILIKE 的模糊搜索和分类过滤。所有返回的记忆记录都经过 normalize_memory_record() 标准化处理,统一输出包含 id、memory、preview(截断至 120 字符)、category、category_label、score、时间戳等字段的结构化对象。
离线洞察分析也可通过内部 API 手动触发:
| 方法 | 路径 | 功能 |
|---|---|---|
POST | /api/internal/insights/run | 触发全量每日洞察批任务 |
POST | /api/internal/insights/users/{user_id}/run | 触发单用户洞察任务 |
Sources: app/routers/admin/memories.py, app/routers/internal_insights.py
与 Agent 系统的集成
记忆系统通过两条路径融入 Agent 的运行时:
Agent 输入注入
在 app/agent.py 中,run_agent_with_memory() 和 run_agent_stream_with_memory() 两个函数封装了记忆读写逻辑。记忆上下文通过 _build_agent_input() 拼接到用户消息前面,形成最终的 Agent 输入:
<memory_context> ← search_user_memory() 返回
<runtime_context> ← 运行时上下文(如工具状态)
<user_message> ← 用户原始消息
此外,app/agents/profile_hook.py 中的 inject_user_profile_hook 作为 Team 的 pre_hook,在每次 Agent 运行前将用户结构化 Profile(从数据库获取)注入 additional_context,与语义记忆形成互补——Profile 提供结构化字段(身高、体重、城市等),Mem0 提供非结构化偏好事实。
Sources: app/agent.py, app/agent.py, app/agents/profile_hook.py
记忆影响链路全景
flowchart TB
subgraph DataSources["数据来源"]
DB[(user_profile 表)]
CONV[对话消息]
OF[穿搭记录]
ITM[衣橱单品]
end
subgraph MemoryLayer["记忆层"]
PROFILE[结构化 Profile<br/>身高/体重/城市/风格偏好字段]
MEM0[(Mem0 语义记忆<br/>pgvector)]
end
subgraph AgentRuntime["Agent 运行时"]
HOOK[profile_hook<br/>注入 Profile 到 additional_context]
SEARCH[search_user_memory<br/>注入语义记忆到 input]
AGENT[Agent 推理]
WRITE[add_conversation_memory<br/>写回新记忆]
end
DB --> PROFILE
PROFILE --> HOOK
CONV --> |实时链路| WRITE
CONV --> |离线链路| INSIGHT[InsightAnalyzers]
OF --> INSIGHT
ITM --> INSIGHT
INSIGHT --> |仲裁后| MEM0
WRITE --> MEM0
MEM0 --> SEARCH
HOOK --> AGENT
SEARCH --> AGENT
style MemoryLayer fill:#fff3e0,stroke:#e65100
Sources: app/agents/profile_hook.py, app/memory/ops.py, app/insights/service.py
关键设计决策
为什么默认关闭实时提取
实时提取(mem0_realtime_analysis_enabled=false)默认关闭,核心原因是 记忆质量权衡。实时提取依赖单轮对话中的有限上下文,容易产生碎片化、低质量记忆条目。离线洞察链路则通过多维度交叉分析、历史窗口聚合、冲突仲裁三重保障,产出的记忆在覆盖面和准确性上显著优于单轮提取。
此外,实时提取的 LLM 调用会增加每轮对话的响应延迟,而离线批任务在凌晨空闲时段运行,不影响用户体感。
写入并发控制
_MEM0_WRITE_SEMAPHORE = asyncio.Semaphore(2) 将并发写入限制为 2,因为 Mem0 内部使用同步 psycopg2 连接池,过多并发异步任务会导致连接池耗尽和请求超时。
Profile 与 Mem0 的分工
系统维护两层用户画像:
- - 结构化 Profile(
user_profile表 +inject_user_profile_hook):存储确定性字段(身高、体重、城市等),通过工具调用由 Agent 主动更新 - - 语义记忆(Mem0):存储从对话和行为中自动提取的偏好事实,通过检索自动注入
两者互不替代——Profile 提供机器可读的精确参数,Mem0 提供人类可读的偏好语境。
<a id="19-yong-hu-dong-cha-fen-xi"></a>
---
用户洞察分析
- - Section: 核心模块详解
- - Group: 记忆与洞察
- - Source File:
19-yong-hu-dong-cha-fen-xi.md
用户洞察分析是 outfit_agent 中独立于在线对话链路的离线批处理系统。它每日定时扫描全量用户,从穿搭记录、衣橱单品、对话记录三个维度抽取增量数据,通过 LLM 进行结构化分析,并将聚合结果同步至 Mem0 长期记忆,为在线 Agent 的个性化推荐提供持续更新的用户画像输入。该模块完全解耦于主业务流,通过定时调度和内部 API 独立运行。
整体架构
用户洞察系统采用六层分离架构,从配置到调度形成清晰的职责边界。下图展示了各模块之间的数据流向:
graph TD
subgraph 调度层
SCHED[scheduler.py<br/>每日零点 Cron 调度]
AGENDA[AgentOS Scheduler<br/>agno DB 持久化调度记录]
end
subgraph 触发与编排
SEL[selectors.py<br/>InsightSelector<br/>触发条件判定]
SVC[service.py<br/>InsightService<br/>单用户任务编排]
end
subgraph 数据采集
COL[collectors.py<br/>InsightCollector<br/>三维度原始数据采集]
end
subgraph LLM 分析
ANA[analyzers.py<br/>InsightAnalyzers<br/>四类分析器]
PROMPTS[data/prompts/memory/<br/>insight_*.txt 提示词]
end
subgraph 持久化
REPO[repo.py<br/>InsightRepo<br/>user_insights 表读写]
SYNC[mem0_sync.py<br/>Mem0Synchronizer<br/>差量同步到 Mem0]
end
subgraph 管理接口
INTAPI[routers/internal_insights.py<br/>内部触发 API]
ADMIN[routers/admin/insights.py<br/>Dashboard 查询 API]
end
AGENDA --> SCHED
SCHED --> INTAPI
INTAPI --> SVC
SVC --> SEL
SVC --> COL
SVC --> ANA
ANA -.-> PROMPTS
SVC --> REPO
SVC --> SYNC
ADMIN --> REPO
设计要点:调度器通过 AgentOS 的 DB 持久化调度表管理 cron 计划,每日触发内部 API 端点 POST /api/internal/insights/run,由 InsightService 编排单用户的完整洞察流程。分析器和记忆同步器均采用应用级单例,避免在用户循环中反复创建 LLM 客户端。
Sources: __init__.py, service.py, scheduler.py
配置体系
洞察系统的全部阈值和调度参数集中在 InsightConfig 中,通过 app/config.py 的 Settings 统一读取环境变量。配置为不可变数据类(frozen=True),全局通过 get_insight_config() 的 LRU 缓存单例获取。
| 配置项 | 默认值 | 说明 |
|---|---|---|
insight_outfit_delta_threshold | 20 | 距上次 outfit 洞察新增穿搭触发数 |
insight_item_delta_threshold | 20 | 距上次 item 洞察新增衣橱单品触发数 |
insight_chat_interval_days | 30 | 距上次 chat 洞察的最小间隔天数 |
insight_merge_history_days | 730 | merge 时 outfit/chat 历史窗口(2年) |
insight_tone_history_days | 730 | tone 汇总历史窗口(2年) |
insight_chat_overlap_messages | 5 | 对话回溯冗余消息数 |
insight_scheduler_cron | 0 0 * | 每日零点执行 |
insight_scheduler_timezone | Asia/Shanghai | 调度时区 |
insight_batch_size | 200 | 每批扫描用户数 |
insight_user_concurrency | 5 | 同时处理用户并发数 |
配置类还提供了 batch_day_window() 方法,用于将 UTC 时间转换为调度时区的当日窗口(用于幂等写入去重)。chat_interval、merge_history_window、tone_history_window 三个属性分别返回对应的 timedelta 对象。
Sources: config.py, config.py#L216-L226
触发判定机制
InsightSelector 负责判定哪些用户需要在当次批处理中执行洞察。判定逻辑基于三维独立触发,满足任一条件即入队:
flowchart TD
START[遍历用户列表] --> GET_LAST[查询各维度最近洞察时间]
GET_LAST --> COUNT_O[计算新增 outfit 数]
GET_LAST --> COUNT_I[计算新增 item 数]
GET_LAST --> COUNT_C[计算新增消息数]
COUNT_O --> CHK_O{outfit_delta >= 20?}
COUNT_I --> CHK_I{item_delta >= 20?}
COUNT_C --> CHK_C1{有消息?}
CHK_C1 -->|是| CHK_C2{距上次 >= 30天?}
CHK_C1 -->|否| SKIP[跳过该用户]
CHK_O -->|是| ENQUEUE[加入洞察队列]
CHK_I -->|是| ENQUEUE
CHK_C2 -->|是| ENQUEUE
CHK_O -->|否| CONT1{其他条件?}
CHK_I -->|否| CONT1
CHK_C2 -->|否| CONT1
CONT1 -->|全部不满足| SKIP
CONT1 -->|有满足| ENQUEUE
InsightTrigger 数据类封装了触发结果,包含 run_outfit、run_item、run_chat 三个布尔标志以及触发原因列表 reasons(取值为 outfit_delta、item_delta、chat_interval)。首次洞察的特殊处理:若用户从未做过洞察(对应维度的 last_*_at 为 None),则使用全量计数进行阈值比较——即新用户只要累计穿搭或单品数达到阈值即可触发。
衣橱单品的触发计数仅统计 source in ('wardrobe', 'wardrobe_want') 且 status != 'disposed' 的新增记录,排除已丢弃的单品。
Sources: selectors.py
数据采集层
InsightCollector 封装了三个维度的原始数据抽取逻辑,每个方法接收 session 和时间窗口参数,返回结构化的字典负载。
穿搭数据采集
collect_outfit_data() 仅抽取自上次洞察以来新增的穿搭记录。采集链路为:outfits → 关联 cover_image → outfit_items → 组成 items → 单品 images。返回的每条记录包含基础字段(id、created_at、source、status、title)、行为信号(view_count、liked_at、disliked_at)、生成上下文(prompt_data、actions、wear_city)以及完整的组成单品信息(品类、颜色、材质、风格描述和关联图片)。
衣橱数据采集
collect_item_data() 分析当前全量有效衣橱(而非仅新增单品)。过滤条件为 source in ('wardrobe', 'wardrobe_want') 且 status != 'disposed'。每条记录包含完整的商品属性(品类、品牌、价格)和风格标签体系(item_description、visual_aesthetics、silhouette_and_cut、design_and_details、materials、color、occasion_functionality)。
对话数据采集
collect_chat_data() 采用会话内回溯策略:首先找出时间窗口内有新增消息的所有 conv_id,然后对每个会话获取新增消息并向前冗余 5 条(按 created_at DESC, id DESC LIMIT 5),合并去重后按时间正序排列。每条消息同时保留文本 content 和结构化 extra.jsonValue,确保卡片消息、商品消息等格式化内容也能被分析器理解。
Sources: collectors.py
LLM 分析器
InsightAnalyzers 是全局单例(通过 get_insight_analyzers() 获取),封装了五类分析能力。所有分析器复用 get_model("flash") 获取 LLM 模型实例,提示词通过 get_prompt() 从 data/prompts/memory/ 动态加载,支持中英文。
分析器调用矩阵
| 分析器方法 | 提示词文件 | 输入格式 | 输出格式 | 触发时机 |
|---|---|---|---|---|
analyze_outfit() | insight_outfit.txt | JSON 穿搭记录 | <csv> 块 | 满足 outfit 触发 |
analyze_item() | insight_item.txt | JSON 衣橱数据 | <csv> 块 | 满足 item 触发 |
analyze_chat() | insight_chat.txt | 格式化消息文本 | <csv> 块 | 满足 chat 触发 |
analyze_tone() | tone.txt | 消息快照 | 纯文本 | chat 成功后 |
analyze_merge() | insight_merge.txt | 聚合负载 | <yaml> 块 | 维度分析完成后 |
输出解析与校验
分析器的输出统一通过 extract_tagged_block() 进行正则提取,确保返回内容严格包裹在指定 XML 标签内。对于 merge 分析器,输出必须为合法 YAML,顶层仅有 actions 键,每项动作仅允许 action、memory_id、content 三个字段。
聊天负载渲染
render_chat_payload() 将消息列表转换为可读文本,每条消息包含时间戳、会话 ID、渠道、角色、消息类型等头部信息,并对结构化 jsonValue 进行层级展开——提取 type、payload_type、payload_text、payload_records 等关键字段,避免将原始 JSON 裸传给模型。
Merge 负载组装
render_merge_payload() 将四部分数据组装为分段文本:[CurrentMemories](当前 Mem0 记忆)、[OutfitInsights](近两年穿搭洞察)、[LatestItemInsight](最新衣橱洞察)、[ChatInsights](近两年对话洞察)。其中穿搭和对话使用历史全量窗口,衣橱仅使用最新一条。
Sources: analyzers.py
提示词设计
洞察系统的四类提示词均位于 data/prompts/memory/ 目录,采用中英文双语版本。每份提示词定义了严格的角色定位、分析规则和输出协议。
维度分析提示词
三个维度分析提示词(insight_outfit、insight_item、insight_chat)共享相同的输出约束——仅允许输出 <csv> 块,格式为 Dimension, Confidence, Observation 三列。各维度允许的 Dimension 字段范围不同:
| 维度 | 允许的 Dimension 字段 |
|---|---|
| 穿搭 (outfit) | Style_Volatility_Index、Aesthetic_Targets、VTO_Positive_Triggers、VTO_Negative_Triggers、personal_taboos、cultural_taboos |
| 衣橱 (item) | Color_Palette_Distribution、Signature_Pieces、Blind_Spot、Wardrobe_Balance、Brand_Affinity、Price_Range |
| 对话 (chat) | flatter_areas、highlight_areas、personal_taboos、cultural_taboos、Aesthetic_Targets、Signature_Pieces、Blind_Spot、Brand_Affinity、Price_Range |
Merge 仲裁提示词
insight_merge 的角色是"记忆仲裁引擎",核心哲学为"行为代表过去的舒适区,语言宣告当下的意图"。它接收当前 Mem0 记忆和三维度洞察历史,输出差量化的 add/update/delete 动作列表。提示词定义了 7 大冲突仲裁法则(身体密码覆盖、审美向往混合法则、绝对雷区一票否决、预算波动优先、品牌倾向验证、执念单品冷却、盲区破壁安全网),确保记忆更新遵循"行为优先、语言当下的意图让路"原则。
Tone 提示词
tone.txt 提示词用于分析用户的沟通风格特征,从对话记录中提取语调偏好,结果最终写入用户 Profile 的 extra.tone 字段。
Sources: insight_outfit.zh.txt, insight_item.zh.txt, insight_chat.zh.txt, insight_merge.zh.txt
Mem0 同步层
Mem0Synchronizer 负责将 merge 分析结果转化为对 Mem0 记忆库的差量操作。整个流程为:解析 YAML → 校验动作合法性 → 校验 memory_id 归属 → 顺序执行变更。
动作解析与校验
parse_actions() 从 merge 输出中提取 <yaml> 块,通过 yaml.safe_load() 解析后执行严格校验:
- - 顶层仅允许
actions键 - - 每项动作仅允许
action、memory_id、content三个字段 - -
action仅允许add、update、delete - -
update/delete必须提供memory_id - -
add/update必须提供非空content
差量执行
apply_actions() 执行实际的 Mem0 写入。执行前会校验所有 update/delete 动作的 memory_id 属于当前用户(通过比对 current_ids 集合)。写入操作使用 agent_id=AGENT_ID(即 "outfit-agent")隔离作用域。若 Mem0 未启用,仅记录动作但不实际执行,返回 memory_enabled=False。
执行完成后返回统计结果,包含 add_count、update_count、delete_count 和 current_memory_count。
Sources: mem0_sync.py, ops.py
持久化与数据模型
user_insights 表
洞察数据存储在 user_insights 表中,结构极简但足够支撑完整的洞察生命周期:
| 字段 | 类型 | 说明 |
|---|---|---|
user_id | BIGINT NOT NULL | 用户 ID |
category | VARCHAR(32) NOT NULL | 取值:outfit、item、chat、tone、merge |
updated_at | TIMESTAMPTZ NOT NULL | 洞察生成时间 |
content | TEXT NOT NULL | LLM 分析结果原文 |
支持五种 category 值:outfit(穿搭洞察)、item(衣橱洞察)、chat(对话洞察)、tone(语调分析)、merge(聚合仲裁)。索引 idx_user_insights_user_category_time(user_id, category, updated_at DESC) 支撑高频的"按用户+维度查最新"查询模式。
幂等写入策略
InsightRepo.save_for_batch() 采用当日窗口去重策略:写入前先删除同一用户、同一 category、同一调度日窗口内的旧记录,再插入新记录。这确保了每日重复执行不会产生重复数据,同时保留历史日期的记录供审计和 merge 回溯使用。
Schema 自动初始化
ensure_insight_schema() 在应用启动时执行,自动创建 user_insights 表和相关索引(包括 outfits、items、messages 表的辅助索引)。该函数在 app/main.py 的启动事件中被调用。
单用户任务编排
InsightService.run_for_user() 是单用户洞察的完整编排入口。下图展示了从触发判定到最终持久化的完整流程:
flowchart TD
START[run_for_user] --> TRACE[创建 Trace Span]
TRACE --> TRIGGER{触发判定}
TRIGGER -->|未触发| SKIP[返回 skipped]
TRIGGER -->|触发| RESOLVE_LANG[解析用户语言]
RESOLVE_LANG --> O_CHK{run_outfit?}
O_CHK -->|是| O_COL[采集穿搭数据]
O_COL --> O_ANA[LLM 穿搭分析]
O_ANA --> O_SAVE[写入 user_insights]
O_CHK -->|否| I_CHK{run_item?}
O_SAVE --> I_CHK
I_CHK -->|是| I_COL[采集衣橱数据]
I_COL --> I_ANA[LLM 衣橱分析]
I_ANA --> I_SAVE[写入 user_insights]
I_CHK -->|否| C_CHK{run_chat?}
I_SAVE --> C_CHK
C_CHK -->|是| C_COL[采集对话数据]
C_COL --> C_ANA[LLM 对话分析]
C_ANA --> C_SAVE[写入 user_insights]
C_SAVE --> T_ANA[LLT Tone 分析]
T_ANA --> T_SAVE[写入 tone 洞察]
C_CHK -->|否| MERGE_CHK{任一维度成功?}
T_SAVE --> MERGE_CHK
MERGE_CHK -->|是| MERGE[聚合历史洞察 + Mem0]
MERGE --> MERGE_ANA[LLM Merge 仲裁]
MERGE_ANA --> MEM0_SYNC[Mem0 差量同步]
MEM0_SYNC --> MERGE_SAVE[写入 merge 洞察]
MERGE_CHK -->|否| RESULT[返回结果]
MERGE_SAVE --> TONE_CHK{tone 成功?}
TONE_CHK -->|是| TONE_SUM[汇总 Tone → 用户 Profile]
TONE_CHK -->|否| RESULT
TONE_SUM --> RESULT
语言解析策略:系统通过 _resolve_user_language() 三级推断用户偏好语言——首先从 UserProfile.extra 中查找语言字段,其次从最近 20 条用户消息的 extra 中提取,最后通过中文字符与英文字母的比例统计推断。默认回退为 zh。
错误容忍:每个维度的分析独立 try-catch,单个维度失败不影响其他维度和后续 merge。失败的维度记入 failed_categories,最终状态为 "failed" 但已完成的维度结果仍会持久化。
Tone 分析:当对话洞察成功执行后,额外触发 analyze_tone() 分析用户语调特征。分析结果通过 summarize_tone() 汇总后写入 UserProfile.extra.tone 字段,供在线 Agent 在回复时参考。
Sources: service.py
每日调度与并发控制
调度注册
ensure_insight_schedule() 在应用启动时调用,通过 AgentOS 的调度数据库(build_agno_db())创建或更新 cron 调度记录。调度配置包括端点 POST /api/internal/insights/run、超时 7200 秒、不重试。若 cron 表达式或时区发生变化,自动更新 next_run_at。
批量执行
InsightService.run_daily() 采用分页扫描 + 信号量并发模式:按 batch_size(默认 200)分批加载用户 ID,每批内通过 asyncio.Semaphore(user_concurrency)(默认 5)控制并发。单用户失败不影响批处理继续执行。返回统计包括 scanned_users、queued_users、completed_users、failed_users 和 Mem0 操作计数。
全局锁
run_daily_insights() 使用 asyncio.Lock() 确保同一时间只有一个批任务在执行。若重复调用,直接抛出 RuntimeError,由内部 API 返回 HTTP 409。
Sources: scheduler.py
API 接口
内部触发接口
| 方法 | 路径 | 说明 |
|---|---|---|
POST | /api/internal/insights/run | 手动触发每日洞察批任务,支持 limit_users 参数限制扫描数 |
POST | /api/internal/insights/users/{user_id}/run | 手动触发单用户洞察任务 |
内部接口通过 Bearer Token 认证,支持内部服务 Token 或 Admin Token。
Admin 查询接口
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /api/admin/insights/summary | 全局洞察摘要(总记录数、用户数、最新时间、按 category 分组统计) |
GET | /api/admin/insights | 分页查询,支持 user_id、category、q(全文搜索)过滤 |
GET | /api/admin/insights/users/{user_id}/latest | 获取指定用户各维度的最新洞察快照 |
Admin 接口还支持通过用户管理页面删除指定用户的全部洞察数据(DELETE /api/admin/users/{user_id}/data 中 scope 包含 "insights" 时)。
Sources: internal_insights.py, admin/insights.py
链路追踪与可观测性
每次单用户洞察执行都通过 start_trace_span("user_insight.run", kind="CHAIN") 创建独立的 Trace Span,Span 属性包含用户 ID、触发原因、各维度执行状态和 Mem0 操作计数。所有 LLM 调用通过 LoggedOpenAIChat 记录完整的请求-响应对,支持在 Dashboard 的 Trace 页面中回溯完整的分析过程。
关键 Span 属性包括:user.id、status、trigger_reason、ran_outfit/ran_item/ran_chat/ran_tone(布尔值)、merge_action_add/merge_action_update/merge_action_delete(计数)、tone_profile_updated。
<a id="20-chuan-da-gong-zuo-liu-fu-wu"></a>
---
穿搭工作流服务
- - Section: 核心模块详解
- - Group: 业务服务
- - Source File:
20-chuan-da-gong-zuo-liu-fu-wu.md
穿搭工作流服务是系统中负责从用户需求到完整穿搭结果全链路自动化执行的核心业务模块。它基于 Agno Workflow 框架构建了一个五步流水线——创意构思、单品选择、图片生成、结果分发——将原本需要多轮交互的穿搭生成过程封装为一次同步执行的完整工作流。本文将从架构设计、数据模型、执行流程、状态管理四个维度展开说明,帮助开发者理解整个服务的运行机理与扩展方式。
整体架构
穿搭工作流服务采用三层分离架构:工具层对外暴露 Agent 可调用的 run_outfit_flow 工具,服务层通过 OutfitWorkflowService 编排工作流生命周期与状态持久化,工作流层由 OutfitGenerationWorkflow 承载五步流水线的具体执行逻辑。
graph TB
subgraph 工具层
A["run_outfit_flow<br/>Agent 调用入口"]
end
subgraph 服务层
B["OutfitWorkflowService<br/>单例服务"]
B1["build_request()"]
B2["run_prepared_workflow()"]
B3["recover_interrupted_workflows()"]
end
subgraph 工作流层
C["OutfitGenerationWorkflow<br/>Agno Workflow"]
C1["Step 1: prepare_context"]
C2["Step 2: ideate_outfits"]
C3["Step 3: select_items"]
C4["Step 4: generate_images"]
C5["Step 5: dispatch_results"]
end
subgraph 子服务
D1["OutfitIdeationService"]
D2["OutfitItemSelectionService"]
D3["OutfitSummaryService"]
end
subgraph 外部依赖
E1["Dialog State"]
E2["Agno Session DB"]
E3["Messaging Tools"]
E4["Image Generation"]
end
A -->|"校验确认状态"| B
B -->|"构建请求、持久化状态"| C
C1 --> C2
C2 -->|"LLM 生成方案"| D1
C2 --> C3
C3 -->|"搜索匹配单品"| D2
C3 --> C4
C4 -->|"调用生图"| E4
C4 --> C5
C5 -->|"发送穿搭卡片"| E3
C5 -->|"生成总结文本"| D3
B -->|"写回状态"| E1
B -->|"持久化"| E2
B -->|"启动恢复"| B3
Sources: outfit_workflow_service.py, outfit_generation.py, outfit_workflow_tools.py
数据模型
工作流涉及的 Pydantic 模型定义在 app/workflows/models.py 中,形成一条从用户输入到工作流结果的完整数据链路。
输入侧模型
OutfitTaskPayload 是工作流的输入任务描述,包含任务文本、场景、约束、风格偏好、用户画像备注以及必选单品列表。其中 must_have_items 限制最多 8 项,每一项通过 WorkflowRequiredItem 描述槽位、分类和搜索条件。两个模型都会对 slot 字段做标准化处理,将 coat 映射为 outerwear、shoe 映射为 shoes 等别名统一。
| 字段 | 类型 | 说明 |
|---|---|---|
task_text | str | 用户穿搭需求的文本描述,不可为空 |
scene | dict | 场景信息(场合、城市、天气等) |
constraints | dict | 约束条件(颜色、预算等) |
style_preferences | list[str] | 风格偏好列表 |
user_profile_notes | list[str] | 用户画像相关备注 |
must_have_items | list[WorkflowRequiredItem] | 必选单品,最多 8 项 |
metadata | dict | 元数据 |
OutfitWorkflowRequest 是构建后的完整工作流请求,除了包含 OutfitTaskPayload 外,还携带 run_id、workflow_session_id、user_id、渠道信息和用户头像路径。其中 workflow_session_id 由 agent_session_id 与 run_id 拼接而成,用于独立的 Agno 会话持久化。
Sources: models.py, models.py, models.py
中间产物模型
工作流执行过程中产生的中间数据通过以下模型传递:
- -
OutfitIdea:由 LLM 生成的穿搭方案构思,包含标题、描述、场合、情绪、风格摘要以及 1~8 个OutfitIdeaSlot。每个槽位携带分类信息和搜索关键词query。 - -
SelectedItem:单品选择结果,包含item_id、分类信息、层叠顺序layer_order以及SelectedItemBrief(品牌、描述、价格等摘要)。 - -
WorkflowOutfitResult:单套穿搭的完整结果,聚合了OutfitIdea、SelectedItem列表、生成的outfit_id、image_id和状态。
Sources: models.py
输出模型
WorkflowDispatchResult 是整个工作流的最终输出,包含:
| 字段 | 类型 | 说明 | |
|---|---|---|---|
run_id | str | 本次工作流运行的唯一标识 | |
status | str | completed / partial_failed / failed | |
outfits | list[WorkflowOutfitResult] | 全部穿搭结果 | |
summary_text | `str \ | None` | LLM 生成的总结文案 |
failed_count | int | 失败的穿搭套数 | |
delivery | `dict \ | None` | 分发上下文(渠道、会话、消息 ID) |
Sources: models.py
五步工作流详解
OutfitGenerationWorkflow 继承自 Agno 的 Workflow 基类,注册了五个 Step,每个步骤的执行器是异步方法。步骤之间通过 StepOutput.content 传递字典载荷,当前一步骤的输出自动成为下一步骤的输入。
flowchart LR
S1["1. prepare_context"] --> S2["2. ideate_outfits"]
S2 --> S3["3. select_items"]
S3 --> S4["4. generate_images"]
S4 --> S5["5. dispatch_results"]
style S1 fill:#e8f4f8,stroke:#2196F3
style S2 fill:#fff3e0,stroke:#FF9800
style S3 fill:#f3e5f5,stroke:#9C27B0
style S4 fill:#e8f5e9,stroke:#4CAF50
style S5 fill:#fce4ec,stroke:#E91E63
Step 1:prepare_context(准备上下文)
从输入字典中反序列化 OutfitWorkflowRequest,将其作为后续步骤的请求上下文。这一步本身不做业务计算,是纯粹的数据校验与格式化。
Sources: outfit_generation.py
Step 2:ideate_outfits(创意构思)
调用 OutfitIdeationService.generate_ideas() 生成 2 套穿搭方案。该服务的执行链路如下:
- 1. 构建 LLM 请求:从
data/prompts/workflows/outfit_ideation_workflow_adapter.{lang}.txt加载提示词模板,注入任务 JSON 和构思上下文。 - 2. 构建构思上下文:并行加载用户画像(来自
user_profiles表)和相关记忆(来自 Mem0 向量检索),与must_have_items合并后作为额外上下文。 - 3. 调用 LLM:以
OutfitIdeationResponse作为结构化输出约束,要求 LLM 返回恰好 2 套方案。 - 4. 必选单品保障:遍历生成的方案,将
must_have_items中的固定单品(带item_id、image_id的)强制合并到对应的item_slots中;如果存在多余的非必要槽位则裁剪以保证不超过 8 项上限。
Sources: outfit_generation.py, outfit_ideation_service.py, outfit_ideation_service.py
Step 3:select_items(单品选择)
遍历每套方案的 OutfitIdea,调用 OutfitItemSelectionService.select_items_for_idea() 为其匹配具体单品。选择逻辑分两条路径:
- - 有
item_id的槽位:通过get_item_briefs()批量获取单品摘要,直接使用指定单品。 - - 无
item_id的槽位:通过search_items_batch_core()批量搜索候选,从结果中取第一个未被使用的单品。
每个槽位最多返回 5 个候选,选择结果包含品类三级分类、价格、品牌等摘要信息。选择过程中通过 used_item_ids 集合确保同一穿搭内不重复使用单品。
Sources: outfit_generation.py, outfit_item_selection.py
Step 4:generate_images(图片生成)
使用 asyncio.gather 并行为每套穿搭调用 _generate_single_outfit()。该方法的核心流程:
- 1. 构建
add_items:将SelectedItem列表转换为包含item_id、position(标准化后的槽位名)、layer_order、is_required的结构。 - 2. 构建
prompt_data_overrides:注入穿搭任务、创意、场景、约束和选中单品等上下文到图片生成提示词中。 - 3. 调用
generate_outfit_by():这是底层的穿搭图生成函数,负责调用图片生成服务并持久化结果。关键参数包括is_async=False(同步执行)和invoke_completion_callbacks=False(不触发异步回调,由工作流统一处理分发)。 - 4. 错误处理:单套穿搭的生成失败不会阻断其他套;失败的穿搭会被标记为
status="failed"并记录failure_reason。
Sources: outfit_generation.py, outfit_generation.py
Step 5:dispatch_results(结果分发)
根据图片生成结果的状态分三种情况处理:
| 结果状态 | 处理逻辑 |
|---|---|
全部成功 (completed) | 调用 send_outfits() 发送穿搭卡片 → 调用 OutfitSummaryService 生成总结文案 → 调用 send_text_message() 发送总结 |
部分失败 (partial_failed) | 调用 send_outfits() 发送成功的穿搭 → 调用 send_text_message() 发送部分失败提示 |
全部失败 (failed) | 调用 send_text_message() 发送全部失败提示 |
消息发送操作通过 Agno 的 trace_tool_call 进行链路追踪,确保每个工具调用都有完整的输入输出记录。最终构建 WorkflowDispatchResult 并附带 delivery 上下文信息。
Sources: outfit_generation.py
服务层编排
OutfitWorkflowService 是工作流的编排中心,以单例模式运行,承担请求构建、工作流执行、状态持久化和中断恢复四项职责。
请求构建与执行
服务提供两种执行模式:
- - 同步模式 (
run_workflow):直接等待工作流完成并返回WorkflowDispatchResult,适用于 Agent 工具调用场景。 - - 后台模式 (
start_workflow):通过asyncio.create_task在后台执行工作流,立即返回run_id和workflow_session_id,适用于长时任务场景。
两种模式都通过 build_request() 构建 OutfitWorkflowRequest,其中 run_id 使用 outfit-workflow:{uuid} 格式,workflow_session_id 使用 {agent_session_id}::outfit-workflow::{run_id} 格式。
Sources: outfit_workflow_service.py
状态持久化
工作流的每个执行结果都会通过 persist_dialog_state_update() 持久化到对话状态中:
| 场景 | sop_stage | outfit_status |
|---|---|---|
| 成功完成 | s3_visual_delivered | completed |
| 部分失败 | s3_visual_delivered | partial_failed |
| 执行异常 | outfit_failed | failed |
| 用户新消息打断 | await_outfit_confirmation | awaiting_confirmation |
| 被取消 (CancelledError) | await_outfit_confirmation | awaiting_confirmation |
当工作流因用户发送新消息而被取消时,服务会将状态回退到 await_outfit_confirmation,保留 outfit_task 不变,并设置 pending_confirmation 提示用户可以重新触发。
Sources: outfit_workflow_service.py, outfit_workflow_service.py
中断恢复
系统启动时,recover_interrupted_workflows() 会扫描数据库中所有 outfit_status='running' 的会话,根据最新一次工作流 run 的状态决定恢复策略:
- 1. 工作流已完成(
COMPLETED):直接将结果状态写回对话状态,跳过等待。 - 2. 工作流失败/取消(
ERROR/FAILED/CANCELLED):将状态回退到await_outfit_confirmation,提示用户重试。 - 3. 无最新 run 记录:同样回退到确认状态。
恢复过程通过两步 SQL 查询实现:先查找 outfit_status='running' 的会话,再对每个会话查询其对应的 workflow session 获取最新 run 状态。
Sources: outfit_workflow_service.py, outfit_workflow_service.py, outfit_workflow_service.py
工具层入口
run_outfit_flow 是 Agent 调用工作流的唯一入口,定义在 app/tools/outfit_workflow_tools.py 中。该工具内置了多层前置校验,确保只有在正确的对话状态下才能触发工作流。
前置校验链
flowchart TD
A["run_outfit_flow 被调用"] --> B{ToolContext 可用?}
B -->|否| B1["返回 'User context unavailable'"]
B -->|是| C{session_id 存在?}
C -->|否| C1["返回 'session_id is required'"]
C -->|是| D{已有活跃工作流?}
D -->|是| D1["返回 'outfit_workflow_already_running'"]
D -->|否| E{sop_stage == 'await_outfit_confirmation'<br/>且 action == 'run_outfit_flow'?}
E -->|否| E1["返回 'outfit_flow_not_ready_to_run'"]
E -->|是| F["解析 outfit_task 参数"]
F --> G["执行工作流"]
Sources: outfit_workflow_tools.py
活跃工作流检测
_has_active_outfit_workflow() 通过查询 Agno Session 数据库中对应 workflow session 的最新 run 状态来判断是否有正在执行的工作流。只有当最新 run 的状态不在 COMPLETED、ERROR、CANCELLED、PAUSED 之中时才认为是活跃的。
Sources: outfit_workflow_tools.py
任务参数解析
_normalize_task_payload() 支持三种参数来源的合并:
- 1.
dialog_state.outfit_task:已有对话状态中的基础任务 - 2.
outfit_task_json:显式传入的完整任务 JSON,覆盖基础任务 - 3.
task_text/must_have_items_json:单独覆盖的任务文本和必选单品
所有来源通过 normalize_outfit_task_payload_data() 做统一标准化,包括场景文本解析、列表去重、必选单品按槽位合并等。
Sources: outfit_workflow_tools.py, models.py
执行后状态更新
工具执行完成后,通过 update_dialog_state_fields() 更新对话状态,并返回包含以下关键字段的 JSON:
- -
suppress_main_agent:当有图片成功生成且状态为completed或partial_failed时设为True,告知主 Agent 不需要再生成额外回复(穿搭卡片和总结已由工作流直接发送)。 - -
generate_images_triggered:标记是否有穿搭图片实际生成。
这个机制通过工具元属性 __tool_stop_after_tool_call_if__ 实现,当 suppress_main_agent 为 True 时,Agent 的本轮回复会被截断。
Sources: outfit_workflow_tools.py, outfit_workflow_tools.py
SOP 阶段与对话状态
穿搭工作流与系统的 SOP(标准操作流程)阶段紧密集成。工作流涉及的 sop_stage 变化如下:
stateDiagram-v2
[*] --> await_outfit_confirmation: 信息收集完成
await_outfit_confirmation --> outfit_running: run_outfit_flow 执行
outfit_running --> s3_visual_delivered: 成功/部分成功
outfit_running --> outfit_failed: 全部失败/异常
outfit_running --> await_outfit_confirmation: 用户新消息打断
outfit_failed --> await_outfit_confirmation: 重试
s3_visual_delivered --> [*]
dialog_state 中与工作流相关的字段包括:
| 字段 | 说明 |
|---|---|
sop_stage | 当前 SOP 阶段 |
outfit_task | 任务清单(OutfitTaskPayload 的 JSON 表示) |
outfit_status | 工作流状态:running / completed / partial_failed / failed / awaiting_confirmation |
outfit_run_id | 当前/最近一次工作流的 run_id |
outfit_result_summary | 工作流结果摘要 |
pending_confirmation | 待确认事项(action + summary) |
值得注意的是,run_outfit_flow 工具被加入 ALWAYS_ALLOWED_TOOLS 集合,不受 skill 门控限制,确保在任何 skill 激活状态下都可以触发工作流。
Sources: dialog_state.py, dialog_state.py, outfit_workflow_service.py
特殊模式:创意图测模式
当调用者的 user_id 等于 _OUTFIT_IDEATION_IMAGE_TEST_USER_ID(硬编码为 46)时,工作流会进入创意图测模式,跳过标准的五步流水线,直接执行:
- 1. 调用
OutfitIdeationService生成 2 套创意 - 2. 为每套创意调用 LLM 生成图片提示词
- 3. 调用
generate_image()生成图片 - 4. 返回包含
ideas、images、outfits的结果
这个模式主要用于内部测试 LLM 的图片提示词质量,不经过单品选择流程,直接基于创意描述生成图片。
Sources: outfit_workflow_tools.py, outfit_workflow_tools.py
国际化支持
工作流相关的用户可见文案通过 data/i18n/workflow.{lang}.json 管理,涵盖以下场景:
| 键名 | 用途 |
|---|---|
partial_failure | 部分失败时的用户提示 |
all_failed | 全部失败时的用户提示 |
started_ack | 工作流开始时的确认消息 |
interrupted_resume_prompt | 服务重启后恢复提示 |
followup_interrupt_pending_confirmation | 用户新消息打断时的待确认说明 |
already_running_error | 重复触发时的错误提示 |
所有文案支持 {success_count} 和 {failed_count} 等占位符动态替换。
Sources: workflow.zh.json, workflow.en.json
运行时上下文构建
工作流内部的工具调用(如 send_outfits、send_text_message)需要一个模拟的 RunContext。build_internal_run_context() 通过 SimpleNamespace 构建一个轻量级上下文,包含 session_id、run_id、user_id 和注入了 tool_ctx 的 session_state,使得消息发送工具能够正常工作。
Sources: runtime.py
与其他模块的关系
穿搭工作流服务在系统中的位置可参考以下页面:
- - 工作流的触发依赖 Dialog State 设计 中描述的状态机和确认机制
- - 工具门控机制参考 技能工具门控机制
- - 图片生成的具体实现参见 图片生成与处理
- - 工作流结果通过消息渠道发送,消息分发机制参见 会话分发与回复状态
- - 用户画像加载参见 Mem0 记忆管理
<a id="21-tu-pian-sheng-cheng-yu-chu-li"></a>
---
图片生成与处理
- - Section: 核心模块详解
- - Group: 业务服务
- - Source File:
21-tu-pian-sheng-cheng-yu-chu-li.md
图片生成与处理系统是 Outfit Agent 的核心业务模块,负责从用户上传的图片、单品参考图到最终穿搭效果图的全流程处理。该系统采用异步任务队列架构,支持高并发图片生成、智能提示词构建、图像后处理及多渠道分发,为用户提供高质量的虚拟试穿和穿搭生成体验。
系统架构总览
图片生成与处理系统由六个核心子系统组成,形成完整的图片处理流水线。ImgGenHandler 作为后台任务消费器,通过 Redis 队列驱动整个生成流程;Victor Prompt Builder 负责构建高质量的生成提示词;Image Gen Tools 提供 Agent 可调用的图片生成工具;Generated Image Postprocess 对生成的图像进行裁剪、水印等后处理;Image Caption Handler 异步为图片添加语义描述;Image Pipeline 则统一处理用户上传图片的入站流程。
graph TB
subgraph "用户交互层"
A[用户上传图片] --> B[Image Pipeline]
C[Agent 工具调用] --> D[Image Gen Tools]
end
subgraph "任务调度层"
D --> E[Redis 队列]
F[Outfit 工具] --> E
E --> G[ImgGenHandler]
end
subgraph "生成处理层"
G --> H{生成模式判断}
H -->|普通生成| I[Victor Prompt Builder]
H -->|单品编辑| J[Parent Outfit Edit]
H -->|场景编辑| K[Parent Scene Edit]
I --> L[图片生成 API]
J --> L
K --> L
end
subgraph "后处理层"
L --> M[Generated Image Postprocess]
M --> N[比例裁剪]
M --> O[水印添加]
M --> P[OSS 上传]
end
subgraph "分发层"
G --> Q[ImgGen Callback]
Q --> R[渠道投递]
Q --> S[消息持久化]
Q --> T[摘要生成]
end
subgraph "语义增强层"
U[新图片入库] --> V[Image Caption Handler]
V --> W[描述生成]
V --> X[向量计算]
end
style A fill:#e1f5fe
style C fill:#e1f5fe
style G fill:#fff3e0
style M fill:#f3e5f5
style Q fill:#e8f5e8
style V fill:#fce4ec
Sources: app/img_gen_handler.py, app/img_gen_queue.py, app/tools/image_gen_tools.py, app/generated_image_postprocess.py, app/img_gen_callback.py, app/image_caption_handler.py, app/messaging/image_pipeline.py
ImgGenHandler:异步图片生成引擎
ImgGenHandler 是图片生成系统的核心调度器,采用生产者-消费者模式实现异步图片生成。该组件在应用启动时初始化并持续运行,通过 Redis 队列获取待处理的穿搭任务,调用图片生成 API,并在完成后触发回调通知。
初始化与配置
ImgGenHandler 在应用启动阶段创建,支持以下关键配置参数:
| 参数名 | 默认值 | 说明 |
|---|---|---|
max_concurrency | 3 | 最大并发生成任务数 |
poll_interval | 1.0s | 队列轮询间隔 |
db_reconcile_interval_seconds | 180s | 数据库对账间隔 |
db_batch_size | 100 | 单次数据库查询批次大小 |
queued_ttl_seconds | 600s | 队列任务过期时间 |
processing_lock_ttl_seconds | 1800s | 处理锁过期时间 |
初始化时,系统会注册 on_outfit_completed 回调函数,用于在图片生成完成后执行渠道投递和消息持久化。这些配置参数通过 Settings 对象从环境变量或配置文件中读取,允许根据部署环境进行调整。
Sources: app/img_gen_handler.py, app/config.py, app/main.py
轮询与调度机制
ImgGenHandler 的核心是一个异步轮询循环 _poll_loop,该循环持续运行直到收到停止信号。轮询循环的工作流程如下:
- 1. 定期对账:每
db_reconcile_interval_seconds秒执行一次数据库对账,将status=pending的穿搭记录同步到 Redis 阘列 - 2. 任务获取:从 Redis 队列中弹出一个待处理的
outfit_id,使用阻塞弹出操作(brpop)避免忙等待 - 3. 任务分发:为每个
outfit_id创建一个异步任务,通过信号量控制并发数 - 4. 处理锁获取:在开始处理前获取 Redis 分布式锁,防止重复处理
- 5. 状态更新:将穿搭状态从
pending更新为generating,生成完成后更新为completed或failed
sequenceDiagram
participant Poll as 轮询循环
participant DB as 数据库
participant Redis as Redis 队列
participant Worker as 工作协程
participant API as 图片生成 API
participant Callback as 回调系统
Poll->>DB: 查询 pending 穿搭
DB-->>Poll: 返回穿搭列表
Poll->>Redis: 入队穿搭 ID
Poll->>Redis: brpop 获取任务
Redis-->>Poll: 返回 outfit_id
Poll->>Worker: 创建工作协程
Worker->>Redis: 获取处理锁
Worker->>DB: 更新状态为 generating
Worker->>Worker: 构建生成提示词
Worker->>API: 调用图片生成
API-->>Worker: 返回生成结果
Worker->>Worker: 执行图像后处理
Worker->>DB: 更新状态为 completed
Worker->>Callback: 触发完成回调
Sources: app/img_gen_handler.py, app/img_gen_handler.py, app/img_gen_handler.py
生成模式
ImgGenHandler 支持三种图片生成模式,根据 prompt_data.generation_mode 字段进行路由:
普通生成模式(默认):使用 Victor Prompt Builder 构建详细的生成提示词,结合用户画像、单品信息和穿搭场景生成完整的穿搭效果图。该模式会调用 LLM 生成专业的摄影指导提示词,确保生成效果符合时尚美学标准。
单品编辑模式(parent_outfit_item_edit):基于已有的穿搭图进行单品替换。系统会识别需要移除和添加的单品,构建包含差异信息的提示词,并保留原图中的其他元素不变。该模式支持用户头像参考,确保人物特征的一致性。
场景编辑模式(parent_outfit_scene_edit):保持穿搭内容不变,仅修改背景场景。用户可以指定新的场景描述(如"咖啡馆窗边"),系统会生成包含场景变更指令的提示词,同时确保服装细节 100% 保留。
每种模式都有对应的提示词模板,通过国际化系统支持中英文:
| 模式 | 提示词模板路径 | 关键特性 |
|---|---|---|
| 普通生成 | victor_director.txt + victor_reference_append.txt | LLM 生成专业提示词 |
| 单品编辑 | outfit_edit_from_parent.txt | 差异化提示,支持头像参考 |
| 场景编辑 | outfit_scene_edit_from_parent.txt | 场景变更指令,服装完全保留 |
Sources: app/img_gen_handler.py, app/img_gen_handler.py, app/img_gen_handler.py
Redis 队列与锁机制
图片生成系统使用 Redis 实现任务队列和分布式锁,确保任务的可靠处理和并发安全。队列系统通过三个 Redis 键模式管理任务状态:
队列键(queue:outfit:image_gen):存储待处理的 outfit_id 列表,使用 lpush 入队、brpop 阻塞弹出。
已入队标记(queue:outfit:image_gen:queued:{outfit_id}):防止同一穿搭重复入队,设置 TTL 自动过期。
处理锁(queue:outfit:image_gen:processing:{outfit_id}):确保同一穿搭不会被多个工作协程同时处理,使用 SET NX 原子操作获取锁。
入队操作 enqueue_outfit_generation 的流程设计了多重防护:
- 1. 检查是否已有处理锁存在,若存在则跳过
- 2. 尝试设置已入队标记(
SET NX),若已存在则跳过 - 3. 将
outfit_id推入队列 - 4. 若推入失败,清理已入队标记并抛出异常
这种设计确保了在分布式环境下,即使多个实例同时尝试入队同一穿搭,也只会有一个成功入队。
Sources: app/img_gen_queue.py
Victor Prompt Builder:智能提示词构建
Victor Prompt Builder 是图片生成系统的提示词工程核心,负责将用户画像、单品信息和穿搭场景转换为高质量的生成提示词。该组件通过 LLM 驱动的 "Director" 角色,生成专业的摄影指导提示词。
上下文构建
build_victor_context 函数收集三类关键信息构建提示词上下文:
用户画像(user_profile):从用户资料中提取性别、体型、身高体重、肤色、发型、脸型等外貌特征,以及城市、职业、风格偏好等个人信息。这些信息帮助生成模型理解用户的整体形象。
穿搭数据(outfit_data):包含穿搭标题、穿着城市、造型师文案、单品列表等。每个单品包含 ref_id、product_type、brand、product_description 等结构化信息。
头像参考(avatar_reference):指向用户头像图片的引用标识,用于在生成过程中保持人物特征的一致性。
提示词生成流程
generate_victor_final_prompt 函数执行完整的提示词生成流程:
- 1. 构建上下文数据结构
- 2. 加载 Director 提示词模板(
victor_director.txt) - 3. 将上下文 JSON 嵌入到提示词中
- 4. 调用 LLM 生成专业的摄影指导文本
- 5. 追加参考图片说明(
victor_reference_append.txt) - 6. 返回最终的生成提示词
LLM 生成的提示词通常包含光线、构图、角度、风格等专业摄影要素,确保生成的穿搭效果图具有商业级质量。
Sources: app/victor_prompt_builder.py, app/victor_prompt_builder.py
图像后处理系统
Generated Image Postprocess 模块负责对生成的图像进行标准化处理,确保输出图像符合系统要求的质量标准。
比例裁剪
系统使用 _crop_image_bytes_to_ratio 函数实现智能比例裁剪:
- 1. 解析目标比例(如 "3:4"、"1:1")
- 2. 计算源图像与目标比例的相似度
- 3. 若相似度误差超过阈值(3%),执行中心裁剪
- 4. 裁剪时保持图像中心区域不变,裁剪多余部分
- 5. 保存裁剪后的图像,保持原始格式
裁剪操作使用 PIL 库实现,支持 JPEG、PNG、WebP 等格式。对于 JPEG 格式,输出质量设置为 100(无损),确保图像细节完整保留。
水印添加
系统根据图像类型自动添加相应的水印:
| 图像类型 | 水印类别 | 说明 |
|---|---|---|
TRYON_REF | tryon | 虚拟试穿图像 |
VIBE_REF | outfit | 穿搭参考图像 |
| 其他 | 无 | 不添加水印 |
水印通过 apply_ai_watermark 函数添加,支持自定义水印内容和位置。
元数据存储
后处理完成后,系统会将裁剪元数据编码到图像的 user_note 字段中,包含以下信息:
- - 目标比例
- - 源比例
- - 输出比例
- - 相似度分数
- - 是否执行了裁剪
- - 是否保留了原图
- - 原图 ID 和 OSS URL(若保留了原图)
这些元数据用于后续的图像管理和质量追溯。
Sources: app/generated_image_postprocess.py, app/generated_image_postprocess.py
图片生成工具接口
Image Gen Tools 模块提供两个核心工具函数,供 Agent 在对话过程中调用:
generate_image
该工具生成单张图片,支持文本提示和可选的参考图片:
async def generate_image(
run_context: RunContext,
prompt: str,
ref_images: list[int] | str | None = None,
image_quality: str | None = None,
) -> str:
参数说明:
- -
prompt:必需的文本生成指令 - -
ref_images:可选的参考图片 ID 列表(最多 9 张) - -
image_quality:可选的图片质量参数(如 "1K"、"2K"、"4K")
处理流程:
- 1. 从
ToolContext获取用户信息 - 2. 校验提示词和参考图片
- 3. 调用业务层图片生成 API
- 4. 执行图像后处理
- 5. 自动发送生成的图片到当前渠道
- 6. 返回包含
image_id、oss_url和发送结果的 JSON
generate_tryon_tool
该工具实现虚拟试穿功能,将穿搭效果图应用到用户头像上:
async def generate_tryon_tool(
run_context: RunContext,
image_id: int,
aspect_ratio: str = "3:4",
previous_image_id: int | None = None,
user_requirements: str | None = None,
image_quality: str | None = None,
) -> str:
参数说明:
- -
image_id:穿搭效果图的 ID - -
aspect_ratio:输出图像比例 - -
previous_image_id:可选的先前试穿图 ID(用于微调) - -
user_requirements:可选的用户特殊要求 - -
image_quality:可选的图片质量参数
处理流程:
- 1. 从
ToolContext获取用户头像路径 - 2. 调用虚拟试穿 API,传入穿搭图和头像
- 3. 支持基于先前试穿结果进行微调
- 4. 执行图像后处理
- 5. 自动发送生成的试穿图到当前渠道
Sources: app/tools/image_gen_tools.py, app/tools/image_gen_tools.py
图片入站处理管道
Image Pipeline 模块提供统一的图片入站处理流程,确保用户上传的图片经过标准化处理后进入系统。
处理流程
run_image_pipeline 函数执行完整的入站处理:
- 1. 内容审核:调用
ensure_image_content_allowed检查图片内容是否符合平台规范 - 2. 重复检测:使用图像向量相似度检测是否为重复图片(阈值 0.97)
- 3. 图片保存:将图片上传到 OSS 并保存元数据到数据库
- 4. 语义描述:调用图片描述服务生成图片的文本描述
- 5. 向量计算:计算图片的语义向量用于后续相似度搜索
重复图片处理
当检测到重复图片时(相似度 > 0.97),系统会:
- - 返回已存在的图片记录
- - 标记
is_duplicate=True - - 跳过 Agent 调用,避免重复处理
- - 构建引用已有图片的 Agent 消息
这种设计既节省了存储空间,又避免了重复处理带来的计算开销。
Sources: app/messaging/image_pipeline.py
图片描述与向量化
Image Caption Handler 是一个后台工作器,负责为没有描述的图片异步生成语义描述和向量表示。
工作机制
该组件采用与 ImgGenHandler 类似的架构:
- 1. 定期对账:每 180 秒扫描数据库,找出需要生成描述的图片
- 2. 队列管理:使用 Redis 队列管理待处理的图片 ID
- 3. 并发控制:通过信号量限制并发处理数(默认 2)
- 4. 处理锁:使用 Redis 分布式锁防止重复处理
处理流程
对每张图片的处理流程如下:
- 1. 从 OSS 获取图片字节
- 2. 调用图片描述服务生成文本描述
- 3. 更新数据库中的
description字段 - 4. 计算描述文本和用户备注的向量表示
- 5. 保存向量到数据库的
description_vec字段
这些向量用于支持基于语义的图片搜索功能,用户可以通过自然语言描述找到相关的穿搭图片。
Sources: app/image_caption_handler.py
完成回调系统
ImgGen Callback 系统负责在图片生成完成后执行后续处理,包括渠道投递、消息持久化和摘要生成。
回调触发时机
回调在以下情况触发:
- - 单个穿搭图片生成完成
- - 数据库对账发现遗漏的回调
- - 所有穿搭图片都已生成完成(成功或失败)
回调处理流程
on_outfit_completed 函数执行完整的回调处理:
- 1. 获取回调锁:使用 Redis 分布式锁确保同一消息的回调不会并发执行
- 2. 加载回调状态:从 Redis 加载之前的回调状态
- 3. 检查终止状态:若已处于终止状态则跳过
- 4. 获取消息上下文:从数据库加载消息和会话信息
- 5. 收集完成结果:查询所有已完成和失败的穿搭记录
- 6. 按顺序投递:按照生成顺序投递穿搭图片到渠道
- 7. 持久化消息:将穿搭消息保存到数据库
- 8. 生成摘要:调用 Agent 生成穿搭摘要文本
- 9. 发送摘要:将摘要文本发送到渠道
渠道适配
回调系统支持两种主要渠道的差异化处理:
微信客服(WECHAT_KEFU):
- - 按顺序逐个投递穿搭图片
- - 使用微信客服 API 发送图片消息
- - 需要缓存的
open_kfid和external_userid
微信小程序(WECHAT_MINIAPP):
- - 支持 WebSocket 实时推送
- - 检查 WebSocket 连接是否活跃
- - 若连接活跃则委托 WebSocket 推送,否则使用 HTTP 投递
Sources: app/img_gen_callback.py, app/img_gen_callback.py
配置参数参考
图片生成系统的配置参数定义在 Settings 类中,支持通过环境变量或配置文件进行调整:
| 配置项 | 默认值 | 说明 |
|---|---|---|
img_gen_max_concurrency | 3 | 图片生成最大并发数 |
img_gen_queue_pop_timeout_seconds | 1 | 队列弹出超时时间(秒) |
img_gen_db_reconcile_interval_seconds | 180 | 数据库对账间隔(秒) |
img_gen_db_batch_size | 100 | 数据库对账批次大小 |
img_gen_queue_queued_ttl_seconds | 600 | 队列任务 TTL(秒) |
img_gen_processing_lock_ttl_seconds | 1800 | 处理锁 TTL(秒) |
image_caption_max_concurrency | 2 | 图片描述生成并发数 |
image_caption_poll_interval_seconds | 1.0 | 图片描述轮询间隔(秒) |
image_caption_db_reconcile_interval_seconds | 180 | 图片描述对账间隔(秒) |
image_caption_batch_size | 50 | 图片描述对账批次大小 |
image_caption_queue_queued_ttl_seconds | 600 | 图片描述队列 TTL(秒) |
image_caption_processing_lock_ttl_seconds | 1800 | 图片描述处理锁 TTL(秒) |
这些参数需要根据实际部署环境的负载情况进行调整。高并发场景下,建议适当增加 img_gen_max_concurrency 和 image_caption_max_concurrency 的值。
Sources: app/config.py
用户头像与外貌提取
User Profile Vision 模块负责从用户头像中提取外貌特征,为图片生成提供个性化的参考信息。
提取的特征维度
系统通过视觉模型分析用户头像,提取以下外貌特征:
| 特征类别 | 具体字段 | 说明 |
|---|---|---|
| 性别 | gender, gender_confidence | 性别及置信度 |
| 面部 | face_visibility, face_shape | 面部可见性、脸型 |
| 身体 | body_type, body_frame, shoulder_hip_balance | 体型、骨架、肩臀比例 |
| 肤色 | skin_tone_label, skin_hex, skin_undertone | 肤色标签、十六进制色值、色调 |
| 发型 | hair_style, hair_length, hair_texture, hair_color | 发型、长度、质感、颜色 |
| 其他 | body_features, visible_features | 身体特征、可见特征 |
应用场景
提取的外貌特征在图片生成中的应用:
- 1. 头像参考:将用户头像作为参考图片传入生成模型
- 2. 提示词增强:在 Victor Prompt 中包含用户的外貌描述
- 3. 体型适配:根据体型选择合适的模型图片(普通/Plus)
- 4. 肤色匹配:确保生成的人物肤色与用户一致
系统还支持 BMI 计算,当用户 BMI 超过阈值(25.0)时,自动切换到 Plus 尺寸的模型图片。
Sources: app/user_profile_vision.py, app/user_profile_vision.py, app/tools/context.py
图片搜索工具
Image Search Tools 模块提供基于视觉相似度的图片搜索功能,支持多维度的图片检索。
搜索能力
search_images_tool 函数支持以下搜索维度:
- - 视觉相似度:基于图片向量的相似度搜索
- - 来源过滤:
uploaded(用户上传)、generated(系统生成)、web(网络图片) - - 类型过滤:
vibe_ref(穿搭参考)、ootd(每日穿搭)、item_standard(单品标准图) - - 分类过滤:支持三级分类(
product_type、sub_category、leaf_category) - - 用户过滤:仅搜索当前用户的图片
搜索流程
- 1. 从数据库加载参考图片
- 2. 获取参考图片的向量表示(
image_vec) - 3. 若无向量,使用 OSS 路径计算向量
- 4. 调用相似度搜索 API
- 5. 返回匹配结果列表,包含
image_id、oss_url、score等信息
该工具可用于查找灵感图片、相似穿搭、单品参考等场景。
Sources: app/tools/image_search_tools.py
测试与调试
图片生成系统的测试覆盖了核心流程的各个环节,主要测试文件包括:
单元测试
test_img_gen_flow.py:包含图片生成流程的完整测试,覆盖:
- - Redis 队列的去重和重入队机制
- - 穿搭生成工具的参数处理
- - 同步和异步生成模式
- - 父穿搭编辑和场景编辑模式
- - Victor Prompt 的生成和回退机制
- - 图片生成工具的参考图片校验
测试策略
测试使用 Fake 对象模拟外部依赖:
- - FakeRedis:模拟 Redis 队列和锁操作
- - FakeSession:模拟数据库会话
- - FakeImageRepo:模拟图片仓库
- - FakeSavedOutfit:模拟保存的穿搭记录
通过 monkeypatch 替换真实的外部调用,确保测试的隔离性和可重复性。
调试建议
- 1. 查看生成提示词:在 trace 中检查
victor_promptspan 的输入输出 - 2. 监控队列状态:使用 Redis CLI 查看队列长度和处理锁状态
- 3. 检查回调状态:查看 Redis 中的
img_gen:callback:state:{msg_id}键 - 4. 分析后处理日志:搜索 "Generated image postprocess metrics" 日志条目
Sources: tests/test_img_gen_flow.py, tests/test_img_gen_flow.py
相关文档
- - 穿搭工作流服务:了解穿搭生成的完整业务流程
- - Dialog State 设计:理解对话状态如何影响图片生成
- - 工具注册与工厂:了解工具系统的整体架构
- - 链路追踪与 Trace:监控图片生成的性能指标
<a id="22-dan-pin-chu-shi-hua-chu-li"></a>
---
单品初始化处理
- - Section: 核心模块详解
- - Group: 业务服务
- - Source File:
22-dan-pin-chu-shi-hua-chu-li.md
单品初始化处理是穿搭系统中的核心后台服务模块,负责在用户保存衣物后异步完成标准化图片生成和属性补全。该系统采用 Redis 队列驱动的后台工作者架构,通过两阶段处理流程(标准化 → VLM 解析)将用户上传的原始图片转化为结构化的可检索单品数据,为后续的穿搭推荐、相似搜索等功能提供高质量的数据基础。
架构概览
单品初始化处理系统采用生产者-消费者模式,由三个核心组件协同工作:工具层负责入队、队列层负责任务管理、处理器层负责执行。整个流程从用户保存单品开始,经过 Redis 队列缓冲,最终由后台工作者完成两阶段的属性丰富化处理。
graph TB
subgraph "入队入口"
A[save_item 工具] -->|enqueue_item_processing| D[Redis 队列]
B[数据库对账] -->|reconcile_pending_items| D
end
subgraph "Redis 队列层"
D[queue:outfit:item_init] -->|brpop| E[ItemProcessingHandler]
F[queue:outfit:item_init:queued:{id}] -->|去重控制| D
G[queue:outfit:item_init:processing:{id}] -->|分布式锁| E
end
subgraph "处理阶段"
E -->|claim_item| H[阶段一: 标准化 Normalizing]
H -->|generate_standard_image| I[阶段二: VLM 解析]
I -->|extract_item_attributes| J[向量回写]
J -->|update_vectors| K[完成]
end
subgraph "外部服务"
H --> L[图片生成服务]
I --> M[Vision LLM]
J --> N[Embedding 服务]
end
style A fill:#e1f5fe
style B fill:#e1f5fe
style D fill:#fff3e0
style H fill:#e8f5e9
style I fill:#e8f5e9
style J fill:#e8f5e9
Sources: item_init_handler.py, item_init_queue.py
入队触发机制
单品初始化任务的入队发生在用户通过 save_item 工具保存衣物时。工具层在完成数据库写入后,立即调用 enqueue_item_processing 将任务推入 Redis 队列,实现保存与处理的解耦。这种设计确保用户操作能够快速返回,而耗时的图片生成和属性提取在后台异步完成。
入队过程包含严格的去重逻辑:系统首先检查该单品是否正在处理中(通过 processing 键),然后尝试设置一个带 TTL 的 queued 键。只有当两个检查都通过时,才会将任务推入队列。这种双层去重机制有效防止了重复处理导致的资源浪费。
# 入队核心逻辑
async def enqueue_item_processing(
item_id: int,
*,
queued_ttl_seconds: int = 600,
) -> bool:
redis = get_redis()
# 第一层:检查是否正在处理
if await redis.exists(_processing_key(item_id)):
return False
# 第二层:设置去重标记
queued = await redis.set(
_queued_key(item_id), "1", ex=queued_ttl_seconds, nx=True,
)
if not queued:
return False
# 推入队列
await redis.lpush(_QUEUE_KEY, str(item_id))
return True
Sources: item_init_queue.py, item_tools.py
处理器轮询机制
ItemProcessingHandler 是后台处理的核心调度器,在应用启动时初始化并持续运行。它采用异步轮询 + 并发控制的设计,通过信号量(Semaphore)限制同时处理的任务数量,防止单一用户批量上传时占用过多系统资源。
处理器的轮询循环包含两个关键机制:Redis 队列消费和数据库对账。正常情况下,处理器从 Redis 队列中弹出任务进行处理;同时,每隔一定时间(默认 180 秒)会扫描数据库,将因进程重启、Redis 故障等原因遗留在 pending 或 failed 状态的单品重新入队。这种双通道设计确保了任务不会因为瞬时故障而永久丢失。
Sources: item_init_handler.py, config.py
队列数据结构
Redis 队列层使用三种键类型实现完整的任务生命周期管理,每种键承担不同的职责:
| 键模式 | 类型 | 用途 | TTL |
|---|---|---|---|
queue:outfit:item_init | List | 任务队列,存储待处理的 item_id | 无(持久) |
queue:outfit:item_init:queued:{id} | String | 去重标记,防止同一单品重复入队 | 600 秒 |
queue:outfit:item_init:processing:{id} | String | 分布式锁,防止并发处理同一单品 | 1800 秒 |
任务从队列中弹出后,系统会清除 queued 标记并获取 processing 锁。处理完成后,无论成功或失败,都会释放 processing 锁,使该单品可以被重新处理(例如失败重试场景)。
Sources: item_init_queue.py
处理阶段详解
单品初始化包含两个顺序执行的处理阶段,每个阶段都有明确的职责边界和状态标记。系统通过数据库字段 init_status 追踪单品当前所处的处理阶段,状态流转遵循严格的顺序约束。
stateDiagram-v2
[*] --> pending : 单品保存
pending --> normalizing : claim_item
normalizing --> vlm_parsing : 标准图生成完成
vlm_parsing --> completed : VLM 属性提取完成
normalizing --> failed : 阶段一失败
vlm_parsing --> failed : 阶段二失败
failed --> normalizing : 超时重试(300秒后)
阶段一:标准化(Normalizing)
标准化阶段的核心任务是调用图片生成服务,基于用户上传的原始图片生成一张符合系统规范的标准单品图。标准图具有统一的背景、光照和构图风格,为后续的 VLM 属性提取提供一致的视觉输入。
该阶段首先通过 _resolve_source_image_id 确定源图片。系统会优先选择非 AI 生成的原始图片作为输入;如果原始图片已被其他单品关联,则会克隆一份副本。确定源图片后,调用 update_item 服务并传入 regenerate_standard_image: True 选项触发图片生成流程。
Sources: item_init_handler.py, item.py
阶段二:VLM 解析(VLM Parsing)
VLM 解析阶段利用视觉语言模型(Vision-Language Model)从标准图中提取丰富的结构化属性。这些属性覆盖了单品的多个维度,包括分类信息、视觉美学、材质、适用场景等,共 20+ 个字段。
提取过程通过 _extract_item_attributes 函数完成,它将标准图和属性提取提示词发送给 Vision LLM,模型返回结构化的 JSON 响应。系统使用 fill_missing_attributes_only: True 模式,仅填充尚未有值的字段,避免覆盖用户手动输入的数据。
| 属性维度 | 示例字段 | 说明 |
|---|---|---|
| 分类信息 | product_type, sub_category, leaf_category | 三级分类体系 |
| 视觉描述 | visual_aesthetics, silhouette_and_cut | 视觉风格与剪裁 |
| 材质工艺 | materials, composition, washing | 材质成分与护理 |
| 适用场景 | suitable_occasions, climate_function | 场合与气候适配 |
| 设计细节 | design_and_details, pattern_and_print, finish | 设计元素与工艺 |
Sources: item_init_handler.py, item.py, constants.py
向量回写机制
VLM 阶段完成后,系统会自动触发向量回写流程。根据 6 个核心文本维度(item_description、visual_aesthetics、silhouette_and_cut、design_and_details、materials、occasion_functionality)生成对应的嵌入向量,并写入数据库的 *_vec 字段。这些向量是单品语义搜索的基础,使得用户可以通过自然语言描述找到匹配的衣物。
向量回写通过 _refresh_item_vectors 函数实现,它调用 build_item_vector_updates 计算各维度的嵌入值。对于空白或缺失的文本字段,系统会清除对应的旧向量(clear_missing=True),确保搜索结果的准确性。
graph LR
A[VLM 提取属性] --> B[6个文本维度]
B --> C[Embedding 服务]
C --> D[向量写入 DB]
B -->|item_description| E[item_description_vec]
B -->|visual_aesthetics| F[visual_aesthetics_vec]
B -->|silhouette_and_cut| G[silhouette_and_cut_vec]
B -->|design_and_details| H[design_and_details_vec]
B -->|materials| I[materials_vec]
B -->|occasion_functionality| J[occasion_functionality_vec]
style B fill:#e8f5e9
style C fill:#fff3e0
Sources: item_vectors.py, item.py
状态管理与错误处理
系统通过数据库字段 init_status 和 init_error 追踪每个单品的处理状态。状态更新在每个阶段转换时执行,确保即使进程崩溃也能通过数据库对账恢复正确的状态。
错误处理采用状态标记 + 延迟重试策略:当某个阶段执行失败时,系统将状态标记为 failed 并记录错误信息(截断至 500 字符)。数据库对账机制会定期扫描 failed 状态的单品,如果距离上次更新已超过重试延迟(默认 300 秒),则将其重新入队。同时,系统还会处理"僵死"任务——处于 normalizing 或 vlm_parsing 状态超过 1800 秒的单品会被视为处理超时,同样会被重新入队。
| 状态 | 含义 | 触发条件 |
|---|---|---|
pending | 待处理 | 单品保存时设置 |
normalizing | 标准图生成中 | claim_item 成功后 |
vlm_parsing | VLM 属性解析中 | 标准图生成完成 |
completed | 处理完成 | VLM 解析完成 |
failed | 处理失败 | 任意阶段异常 |
Sources: item_init_handler.py, item_repo.py
并发控制与分布式锁
系统在多个层面实施并发控制,确保在多实例部署场景下的正确性:
应用层并发控制使用 asyncio.Semaphore 限制同时处理的任务数量(默认 2),防止单一进程内的资源耗尽。分布式锁使用 Redis 的 SET NX 命令实现,确保同一单品不会被多个进程同时处理。锁的 TTL 设置为 1800 秒,既覆盖了单个任务的最长处理时间,又能在进程异常退出后自动释放。
从任务队列弹出到获取分布式锁之间存在一个竞态窗口——如果在弹出后、获取锁前该单品已被其他进程处理,_claim_item 方法会通过数据库层面的原子更新(UPDATE ... WHERE ... RETURNING)再次验证,确保只有符合条件的进程能够成功认领任务。
Sources: item_init_handler.py, item_repo.py
配置参数
所有配置参数均可通过环境变量覆盖,系统提供了合理的默认值以满足大多数部署场景:
| 参数 | 默认值 | 说明 |
|---|---|---|
item_init_max_concurrency | 2 | 最大并发处理数 |
item_init_poll_interval_seconds | 1.0 | 轮询间隔(秒) |
item_init_db_reconcile_interval_seconds | 180 | 数据库对账间隔(秒) |
item_init_batch_size | 20 | 单次对账批量大小 |
item_init_retry_delay_seconds | 300 | 失败重试延迟(秒) |
item_init_stale_after_seconds | 1800 | 僵死任务超时(秒) |
item_init_queue_queued_ttl_seconds | 600 | 去重标记 TTL(秒) |
item_init_processing_lock_ttl_seconds | 1800 | 处理锁 TTL(秒) |
Sources: config.py
可观测性
系统集成了 OpenTelemetry 追踪,每个处理任务都会创建一个名为 Item.enrichment.background 的追踪 Span,记录单品 ID、用户 ID、来源、初始状态等上下文信息。处理完成后,Span 会附加最终状态信息。这种端到端的追踪能力使得运维团队可以快速定位处理延迟或失败的根因。
此外,系统在关键节点输出结构化日志,包括任务分发、阶段开始/完成、错误详情等,支持日志聚合分析和告警配置。
Sources: item_init_handler.py, item.py
与其他模块的关系
单品初始化处理与以下模块存在紧密的协作关系:
- - 消息入站处理流程:用户通过聊天发送图片时触发单品保存,进而触发初始化处理
- - 图片生成与处理:标准化阶段依赖图片生成服务生成标准单品图
- - 工具注册与工厂:
save_item工具是入队的主要入口 - - 链路追踪与 Trace:处理过程通过 OpenTelemetry 实现全链路追踪
<a id="23-dashboard-guan-li-mian-ban"></a>
---
Dashboard 管理面板
- - Section: 管理与运维
- - Source File:
23-dashboard-guan-li-mian-ban.md
Dashboard 是 Outfit Agent 的运营管理后台,基于 React + Vite + Ant Design 5 构建,后端由 FastAPI 提供 /api/admin/* REST API。它服务于两类核心用户:产品/运营(改 prompt、看资源、管理数据)和 开发/排障(看 traces、tool spans、配置与运行状态)。Dashboard 的核心价值在于:不重启服务即可热更新 Agent 配置、Skills、Prompt、工具描述,并提供实时的运行态观测与数据运维能力。
整体架构
Dashboard 采用经典的前后端分离架构。前端是一个单页应用(SPA),通过 Vite 构建后,由 FastAPI 在生产环境中以静态文件的方式挂载到 /dashboard/ 路径。所有 API 请求统一发往 /api/admin/*,由后端的 Admin Router 进行统一鉴权和分发。
graph TB
subgraph "浏览器"
A["React SPA<br/>/dashboard/*"]
end
subgraph "FastAPI 后端"
B["Admin Router<br/>/api/admin/*"]
C["Admin Auth<br/>Bearer Token 鉴权"]
D["Agent 配置管理"]
E["Skill 管理"]
F["资源中心"]
G["链路追踪"]
H["系统监控"]
I["数据运维"]
J["用户/记忆/OSS"]
end
subgraph "存储层"
K["PostgreSQL<br/>agno schema + 业务表"]
L["Redis"]
M["OSS 对象存储"]
N["Mem0 长期记忆"]
O["本地文件<br/>TOML/TXT/Prompt"]
end
A -->|"HTTP (dev proxy / prod 同源)"| B
B --> C
C --> D & E & F & G & H & I & J
D & E & F --> O
G --> K
H --> K & L & M & N
I & J --> K & M & N
前端开发模式下,Vite 的 dev server 会将 /api/admin 请求反代到 localhost:8000;生产环境下,npm run build 生成的 dist/ 目录被 FastAPI 直接挂载,两者同源,无需额外代理。
Sources: README.md, vite.config.ts, app/main.py
技术栈与依赖
| 层级 | 技术 | 用途 |
|---|---|---|
| UI 框架 | React 19 + TypeScript | 组件化前端 |
| 构建工具 | Vite 8 | 开发服务器(含 API 反代)与生产构建 |
| 组件库 | Ant Design 5 | 表格、表单、布局等 UI 组件 |
| 代码编辑器 | @monaco-editor/react | TOML / 文本编辑(Agent、Skill、Prompt) |
| 路由 | React Router v7 | 前端路由,basename=/dashboard |
| 状态管理 | Zustand + persist | Admin Token 持久化(localStorage) |
| HTTP 客户端 | Axios | 统一注入 Bearer Token,自动处理 401/403 |
| 图表 | Recharts | 监控统计图表(柱状图、饼图) |
| 时间 | dayjs | 时间格式化 |
后端所有 Admin API 统一挂载在 /api/admin 前缀下,通过 AdminRequired 依赖项进行 Token 鉴权。
Sources: README.md, client.ts, auth.py
鉴权机制
Dashboard 的鉴权与普通用户 JWT 完全隔离。所有 Admin API 请求必须携带 Authorization: Bearer <ADMIN_SECRET_KEY> 头。Token 通过环境变量 ADMIN_SECRET_KEY 配置,若该变量为空,所有 Admin 端点将直接返回 403。
前端的认证流程如下:
sequenceDiagram
participant U as 管理员
participant SPA as Dashboard SPA
participant API as /api/admin/*
U->>SPA: 打开 /dashboard/
SPA->>SPA: 检查 Zustand store 中是否有 token
alt 无 token
SPA->>U: 跳转到 /dashboard/login
U->>SPA: 输入 ADMIN_SECRET_KEY
SPA->>API: GET /api/admin/agents(验证 token)
alt Token 有效
API-->>SPA: 200 OK
SPA->>SPA: 存储 token 到 localStorage
SPA->>U: 进入管理面板
else Token 无效
API-->>SPA: 401/403
SPA->>U: 提示错误
end
else 有 token
SPA->>U: 直接进入管理面板
end
Note over SPA,API: 所有后续请求自动携带 Bearer Token
Note over SPA,API: 401/403 响应自动清除 token 并跳转登录页
Token 存储在 localStorage(key: admin-auth),通过 Zustand 的 persist 中间件管理。Axios 拦截器在每个请求的 Authorization 头中自动注入 Token,并在收到 401/403 响应时自动清除状态并重定向到登录页。
Sources: auth.py, auth.ts, client.ts
侧边导航结构
Dashboard 采用经典的侧边栏 + 顶栏布局,包含 13 个功能页面。侧边栏支持桌面端折叠和移动端抽屉模式,并实时检测后端服务健康状态。
| 路由 | 页面名称 | 图标 | 核心职责 |
|---|---|---|---|
/monitor | 系统监控 | BarChart | 服务健康、今日统计、工具/Agent 分布、错误日志 |
/beta-users | 内测用户 | BarChart | 内测用户行为统计、负面反馈收集、修改意图分析 |
/data | 数据管理 | Database | 单品/穿搭/图片统计与列表、数据质量检查、向量刷新 |
/messages | 消息管理 | Message | 会话消息列表、访问控制(黑白名单)、消息拦截规则 |
/users | 用户管理 | User | 用户列表与详情、画像、记忆、洞察、删除预览与清理 |
/traces | 链路追踪 | Search | Session 列表、实时运行监控、Run 工具调用树、dialog_state |
/auto-tests | 自动化测试 | Experiment | 自然语言规则转 JSON、测试用例执行 |
/playground | Playground | Experiment | Prompt 调试沙箱、历史导入、实时对话测试 |
/agents | Agent 配置 | Robot | 主 Agent TOML 编辑、热重载、可用工具/Skills 展示 |
/skills | Skill 管理 | Tool | Skill TOML 编辑、创建/删除、热重载 |
/prompts | 数据资产 | FileText | 资源树浏览、Monaco 编辑、历史版本回滚、灰度预览 |
/settings | 系统配置 | Setting | 运行时配置查看与受限更新、模型灰度规则 |
/oss | OSS 管理 | Cloud | 目录浏览、文件上传/删除/复制、签名预览 URL |
Sources: AppLayout.tsx, App.tsx
核心功能模块
模块 A:Agent 配置管理
Agent 配置页面是管理主 Agent 的核心入口。当前架构采用单主 Agent 模式,仅维护 data/team/main.toml 一个配置文件。页面提供 Monaco 编辑器直接编辑 TOML,支持保存草稿和保存并热重载两种操作模式。
热重载通过 POST /api/admin/agents/reload 触发,调用 app.agent.reload_agents() 函数,重新加载 Agent 配置、绑定的 Skills 和工具清单。页面会展示最近重载的时间和耗时,以及当前绑定的 Skills 和可用工具列表(支持点击复制)。
本地草稿保护机制通过 useLocalDraft Hook 实现,编辑内容自动保存到 localStorage,即使页面刷新也不会丢失未提交的修改。
Sources: AgentConfig.tsx, agents.py
模块 B:Skill 管理
Skill 管理页面展示所有已加载和未加载的 Skill 列表,包括每个 Skill 的 TOML 配置、绑定工具、引用资源和脚本文件。支持以下操作:
- - 查看/编辑 Skill TOML:使用 Monaco 编辑器编辑
data/skills/{skill_id}/skill.toml - - 创建新 Skill:基于
example/skill.toml模板创建 - - 删除 Skill:删除 TOML 文件(不影响运行中实例,需热重载生效)
- - 热重载:重新加载所有 Skill 定义
Skill 的发现机制遍历 data/skills/ 目录下的所有 skill.toml 文件,并与运行时已加载的 Skill 注册表进行对比,区分「已加载」和「仅存在文件」两种状态。
Sources: SkillConfig.tsx, skills.py
模块 C:数据资产(资源中心)
资源中心是 Dashboard 中功能最丰富的模块之一。它自动发现 outfit_agent/data 和 outfit_biz/data 两个数据根目录下的所有资源文件,按分类(Agent/Team、Skill、Prompt、Channel、I18n、Tool、Asset 等)构建树形浏览结构。
资源文件支持两个数据源的覆盖层机制:当 outfit_agent/data 和 outfit_biz/data 中存在同路径文件时,outfit_agent 优先覆盖 outfit_biz,并在前端标注「覆盖」或「被覆盖」标签。
核心能力包括:
| 能力 | 说明 |
|---|---|
| 分类树浏览 | 按 section > directory > file 三级结构组织 |
| Monaco 编辑 | 根据文件扩展名自动选择语言高亮 |
| 历史版本 | 最多保留 10 个历史版本,支持查看和回滚 |
| 缓存清理 | 编辑后自动清除 PromptLoader / i18n 文本缓存 |
| 热应用 | 根据资源类型自动提示热重载动作(reload_agents / reload_skills / reload_tool_descriptions) |
| 灰度预览 | 支持按用户 ID 预览灰度覆盖后的资源内容 |
| 本地草稿 | 未提交的修改自动保存到 localStorage |
Sources: PromptManager.tsx, prompts.py
模块 D:工具描述管理
工具描述文件 data/tools/descriptions.toml 存储了所有注册工具的用户可见描述文案。Dashboard 提供专门的编辑界面,支持 TOML 格式校验(确保包含所有已注册工具的描述且值为字符串类型)。
编辑保存后会自动清除运行时工具缓存,并提示是否需要热重载 Agent。同样支持最多 10 个版本的历史记录和回滚功能。
Sources: tool_descriptions.py
模块 E:系统监控
系统监控页面是 Dashboard 的默认首页,提供三个核心面板:
服务健康状态:并行检查 PostgreSQL、Redis、OSS、Mem0 和 Agent 五个服务的连通性,展示启动时间和运行时长。
今日统计:查询 agno.agno_traces 和 agno.agno_spans 表,展示今日请求总数、平均响应时间、工具调用分布(Top 10 柱状图)和 Agent 运行分布(饼图)。
错误日志:通过内存环形缓冲区(_AdminLogHandler)捕获最近的 WARN/ERROR 级别日志,支持按级别过滤。
页面每 30 秒自动刷新,Header 区域也独立每 30 秒检测服务健康状态。
Sources: Monitor.tsx, monitor.py
模块 F:链路追踪
链路追踪是排障的核心工具,提供三层钻取视图:
- 1. Session 列表:分页展示所有会话,支持按 user_id、session_id、agent_id、status 过滤。每条记录展示最新用户消息、Agent 回复、trace 数量和错误数量。
- 2. Session 详情:点击进入后展示该会话下所有 Run 的摘要,包括 LLM 和工具调用统计。
- 3. Run 详情:展示单次 Run 的完整 span 树,包含 LLM 请求详情(token 统计)、工具调用详情(参数、结果、耗时)。
此外还提供实时运行监控面板,每 4 秒轮询当前进程内正在执行的 Agent Run,展示关键的运行态字段:
| 字段 | 说明 |
|---|---|
active_skill | 当前激活的技能 |
loaded_skills | 已加载的技能列表 |
active_scene | 当前场景 |
sop_stage | SOP 阶段 |
outfit_status | 穿搭工作流状态 |
active_tool_names | 当前运行中的工具 |
这些字段来自 agno_sessions.session_data.session_state.dialog_state,是排查 SOP 阶段推进是否正确的关键依据。
Sources: TraceInspector.tsx, traces.py
模块 G:系统配置
系统配置页面展示当前运行时配置(敏感字段脱敏),并支持更新部分允许运营面调整的参数。
只读配置:模型角色(default/flash/vision/director 等)、Leader Model、OpenAI Base URL、团队参数等。
可更新配置(MUTABLE_FIELDS):
| 字段 | 说明 | 需重启 |
|---|---|---|
mem0_enabled | Mem0 记忆开关 | 否 |
mem0_search_limit | Mem0 搜索条数限制 | 否 |
litellm_redis_cache_enabled | LiteLLM Redis 缓存开关 | 是 |
litellm_redis_ttl | 缓存 TTL | 是 |
wechat_miniprogram_title | 小程序标题 | 否 |
wechat_miniprogram_pagepath | 小程序跳转路径 | 否 |
页面同时集成模型灰度规则管理,支持按用户维度灰度切换不同模型配置。
Sources: SystemSettings.tsx, settings.py
模块 H:数据管理
数据管理页面提供业务数据的全局视图,分为三个 Tab:
- - 单品管理:展示单品统计(总量、公开/用户维度、今日新增、状态分布、向量缺失数、描述缺失数等),支持分页搜索和详情抽屉。详情中可查看单品属性、关联穿搭、关联图片,以及更新状态(active/idle/disposed)。还支持文本向量搜索和图片向量搜索。
- - 穿搭管理:展示穿搭统计(总量、状态分布、缺失图片/向量/关联单品数等),支持分页搜索和详情查看。
- - 图片管理:展示图片统计(总量、来源分布、描述缺失、向量缺失等),支持分页搜索和签名 URL 预览。
页面还提供向量批量刷新功能,可对缺失向量的单品触发重新计算。
Sources: DataManager.tsx, data.py
模块 I:用户管理
用户管理页面提供用户全景视图,包括:
- - 用户总览:总量、今日/7日活跃、有画像/衣橱/穿搭/会话的用户分布、渠道分布、数据完整性检查(孤立记录)
- - 用户列表:支持按 user_id、昵称、openid、渠道、画像/衣橱/穿搭/会话存在性过滤
- - 用户详情抽屉:
- - 基本信息与绑定账号(支持删除单条 OAuth 绑定)
- - 用户画像(occupation、身高体重、风格偏好等 15 个字段)
- - 衣橱概览
- - 会话历史
- - Mem0 记忆(支持查看、编辑、删除单条记忆,以及清空全部记忆)
- - 离线洞察(按 category 展示最新洞察快照,支持手动触发洞察运行)
- - 删除预览与清理:支持按 11 个维度(profile、oauth、items、outfits、images、conversations、insights、memories、sessions、bind_codes、user)选择性删除用户数据,删除前展示影响面预览
Sources: UserManager.tsx, users.py
模块 J:消息管理
消息管理页面整合了三个子功能:
- - 会话消息:按会话维度展示消息列表,支持按 session_id、用户昵称、最新消息内容搜索。每条会话展示消息数、待处理反馈数、最新消息摘要、Run 信息。
- - 访问控制:管理消息准入策略(allow_all / whitelist_only),支持黑白名单的 CRUD,以及授权码的生成、列表和导出。匹配类型支持 user_id、unionid、channel_openid 三种维度。
- - 消息拦截:管理拦截规则(精确匹配、通配符、正则),支持自动回复、反馈收集等动作。还提供反馈消息的查看和人工客服回复功能。
Sources: MessageManagement.tsx, message_management.py, access_control.py, interception.py
模块 K:OSS 管理
OSS 管理页面提供对象存储的文件管理能力:
- - 目录树浏览:递归展示 OSS 目录结构
- - 文件上传:支持单文件和批量上传,自动检测文件名冲突
- - 批量删除:支持选择多个文件/文件夹批量删除
- - 文件复制/移动:支持跨目录复制和移动操作
- - 签名 URL 预览:生成临时签名 URL 用于图片和文件预览
- - ZIP 打包上传:支持 ZIP 文件解压后批量上传,自动跳过 macOS 系统文件
支持的上传文件类型包括:jpg、jpeg、png、gif、webp、pdf、txt、json、mp4、mov。
Sources: OssManager.tsx, oss.py
模块 L:Playground 测试沙箱
Playground 提供一个隔离的 Prompt 调试环境,用于快速验证 Agent 行为:
- - 配置选择:选择渠道(微信小程序/客服)和语言
- - Prompt 预览:查看当前主 Prompt 和渠道 Prompt 的完整内容
- - 历史导入:按 session_id 导入已有会话的历史消息
- - 自定义覆盖:支持临时替换主 Prompt 和渠道 Prompt
- - 实时运行:发送消息并获取 Agent 回复,同时返回 session_id 和 run_id 以便追踪
Sources: Playground.tsx, playground.py
模块 M:自动化测试
自动化测试页面支持自然语言驱动的测试用例编写与执行:
- - 规则翻译:将自然语言描述的规则转换为结构化 JSON 规则
- - 用例执行:输入消息序列和规则,执行自动化测试并返回结果
Sources: AutoTests.tsx, auto_tests.py
前后端 API 映射
前端通过 src/api/ 目录下的 Axios 封装模块与后端通信,每个模块对应一组 Admin API:
| 前端模块 | 后端路由前缀 | 核心端点 |
|---|---|---|
agents.ts | /api/admin/agents | 列出、获取/更新 TOML、热重载 |
skills.ts | /api/admin/skills | 列出、获取/更新 TOML、创建/删除、热重载 |
prompts.ts | /api/admin/prompts | 列出资源、获取/更新内容、历史版本、回滚 |
toolDescriptions.ts | /api/admin/tool-descriptions | 获取/更新描述、热重载、历史版本 |
monitor.ts | /api/admin/monitor | 服务状态、今日统计、错误日志 |
traces.ts | /api/admin/traces | Session 列表/详情、Run 详情、实时 Run 列表 |
settings.ts | /api/admin/settings | 获取/更新运行时配置 |
data.ts | /api/admin/data | 总览、单品/穿搭/图片列表与详情、搜索 |
users.ts | /api/admin/users | 总览、列表、详情、删除预览与清理 |
memories.ts | /api/admin/memories | 列出、摘要、详情、编辑、删除 |
oss.ts | /api/admin/oss | 目录浏览、上传、删除、复制、签名 URL |
gray.ts | /api/admin/gray | 灰度规则 CRUD、预览、缓存刷新 |
playground.ts | /api/admin/playground | 获取配置、导入历史、运行对话 |
accessControl.ts | /api/admin/access-control | 准入策略、黑白名单、授权码 |
interception.ts | /api/admin/interception | 拦截规则 CRUD、反馈列表与回复 |
insights.ts | /api/admin/insights | 洞察摘要、列表、用户最新洞察 |
autoTests.ts | /api/admin/auto-tests | 规则翻译、用例执行 |
contentFiles.ts | /api/admin/content-files | 文件列表、内容读写、替换、历史回滚 |
betaUsers.ts | /api/admin/beta-users | 内测用户统计面板 |
所有 API 模块共用同一个 Axios 实例(client.ts),自动注入 Authorization: Bearer <token> 头,并在 401/403 时自动登出。
Sources: __init__.py, dashboard/src/api/
开发与部署
开发模式
cd dashboard
npm install # 首次安装依赖
npm run dev # 启动开发服务器(端口 5173,含 API 反代)
访问 http://localhost:5173/dashboard/,输入 ADMIN_SECRET_KEY 登录。前提条件是后端服务已在 :8000 运行,且 .env 中已配置 ADMIN_SECRET_KEY。
生产构建与部署
cd dashboard
npm run build # 产物输出到 dist/
构建完成后重启后端服务,FastAPI 会自动将 dashboard/dist/ 挂载为静态文件到 /dashboard/ 路径(通过 StaticFiles(html=True) 支持 SPA 的前端路由)。
Sources: README.md, vite.config.ts, app/main.py
排障指南
当线上出现问题时,建议按以下顺序排查:
- 1. 查看实时运行状态:在「链路追踪」页面的实时 Run 面板中,查看当前 session 的
active_skill、sop_stage、pending_confirmation等字段,确认 Agent 是否停在正确的 SOP 阶段。 - 2. 检查 dialog_state:在 Run 详情中查看
dialog_stateJSON,确认阶段推进是否正确,是否有工具被门控拒绝。 - 3. 查看用户消息与回复:在 Session 详情中查看最新用户消息和 Agent 回复,定位对话卡点。
- 4. 检查系统监控:在「系统监控」页面查看服务健康状态和最近错误日志,排除基础设施故障。
- 5. 检查配置与 Skill:在「Agent 配置」和「Skill 管理」页面确认当前运行的配置是否正确,是否有未生效的修改需要热重载。
- 6. 检查灰度规则:在「系统配置」的模型灰度区域或「数据资产」的灰度预览中,确认目标用户是否命中了预期的灰度覆盖。
下一步阅读
- - 如需了解 Admin API 的详细接口规范,请参阅 Admin API 接口
- - 如需了解链路追踪的底层数据模型,请参阅 链路追踪与 Trace
- - 如需了解 Agent 配置与 Skills 的架构,请参阅 单主 Agent 架构 和 技能加载与管理
- - 如需了解提示词管理的更多细节,请参阅 提示词管理与国际化
<a id="24-admin-api-jie-kou"></a>
---
Admin API 接口
- - Section: 管理与运维
- - Source File:
24-admin-api-jie-kou.md
Outfit Agent 的 Admin API 是面向运维、产品和开发人员的统一管理后台接口层。所有端点挂载在 /api/admin/* 路径下,通过独立的 Bearer Token 认证机制保护,提供从 Agent 配置热重载、资源文件编辑、链路追踪到用户数据运维的完整管理能力。
认证机制
Admin API 使用独立于用户身份体系的 Bearer Token 认证。所有 /api/admin/* 端点都通过 require_admin 依赖注入进行鉴权,该依赖从 AgentSettings.admin_secret_key(环境变量 ADMIN_SECRET_KEY)读取密钥。
认证流程如下:客户端在每个请求中携带 Authorization: Bearer <token> 请求头,服务端比对 token 与配置的密钥是否一致。当 ADMIN_SECRET_KEY 为空时,所有 Admin 端点直接返回 403,表示管理功能未启用。Token 不匹配返回 403 Invalid admin token,缺少请求头则返回 401。
sequenceDiagram
participant Client as Dashboard / curl
participant Router as /api/admin/*
participant Auth as require_admin
participant Settings as AgentSettings
Client->>Router: Authorization: Bearer <token>
Router->>Auth: 依赖注入
Auth->>Settings: 读取 admin_secret_key
alt 密钥为空
Auth-->>Client: 403 Admin access not configured
else Token 不匹配
Auth-->>Client: 403 Invalid admin token
else Token 匹配
Auth-->>Router: 放行
Router-->>Client: 200 响应
end
配置示例(.env 文件):
# 生产环境请使用随机长字符串;留空则禁用 Admin API
ADMIN_SECRET_KEY=your-secure-random-string-here
API 模块总览
Admin API 由 21 个子路由器组成,统一挂载在 /api/admin 前缀下。各模块按职能可划分为配置与热重载、资源管理、链路追踪与调试、用户与数据运维、安全与准入五大类。
graph TB
subgraph Root["/api/admin"]
direction TB
subgraph Config["配置与热重载"]
A1["/agents — 主 Agent 配置"]
A2["/skills — 技能管理"]
A3["/settings — 运行时配置"]
A4["/tool-descriptions — 工具描述"]
A5["/gray — 灰度规则"]
end
subgraph Resource["资源管理"]
B1["/prompts — 资源中心"]
B2["/content-files — 统一文件管理"]
end
subgraph Debug["链路追踪与调试"]
C1["/traces — Trace 检查器"]
C2["/monitor — 系统监控"]
C3["/playground — Playground 调试"]
end
subgraph Ops["用户与数据运维"]
D1["/users — 用户管理"]
D2["/data — 数据管理"]
D3["/memories — 记忆管理"]
D4["/insights — 用户洞察"]
D5["/message-management — 会话消息"]
D6["/oss — OSS 文件管理"]
D7["/beta-users — 内测统计"]
end
subgraph Security["安全与准入"]
E1["/access-control — 消息准入"]
E2["/interception — 消息拦截"]
end
subgraph Test["测试"]
F1["/auto-tests — 自动化测试"]
end
end
Sources: \_\_init\_\_.py
端点速查表
| 模块 | 路径前缀 | 核心能力 | 文件 |
|---|---|---|---|
| 主 Agent | /agents | 查看/编辑 main.toml、热重载 | agents.py |
| 技能 | /skills | 列出/编辑/创建/删除 Skill、热重载 | skills.py |
| 配置 | /settings | 查看运行时配置(脱敏)、更新可变配置项 | settings.py |
| 工具描述 | /tool-descriptions | 编辑 descriptions.toml、版本历史/回滚 | tool_descriptions.py |
| 灰度 | /gray | 灰度规则 CRUD、按用户预览、缓存刷新 | gray.py |
| 资源中心 | /prompts | 自动发现 data/ 资源、编辑/历史/回滚 | prompts.py |
| 文件管理 | /content-files | 统一文件浏览、编辑/替换/热应用/回滚 | content_files.py |
| 链路追踪 | /traces | Session/Run 列表与详情、实时 Run、生图重试 | traces.py |
| 监控 | /monitor | 服务健康检查、今日统计、错误日志 | monitor.py |
| Playground | /playground | 模拟对话调试、导入历史、Prompt 覆盖 | playground.py |
| 用户 | /users | 用户总览、详情、按范围清理数据 | users.py |
| 数据 | /data | 单品/穿搭/图片概览与详情、质量检查 | data.py |
| 记忆 | /memories | Mem0 记忆查询/摘要/编辑/删除 | memories.py |
| 洞察 | /insights | 用户洞察摘要/历史/最新快照 | insights.py |
| 消息管理 | /message-management | 会话消息列表与详情 | message_management.py |
| OSS | /oss | 文件浏览/上传/删除/复制/文件夹操作 | oss.py |
| 内测统计 | /beta-users | 内测用户实时统计面板 | beta_users.py |
| 准入控制 | /access-control | 策略/黑白名单/授权码管理 | access_control.py |
| 消息拦截 | /interception | 拦截规则 CRUD、反馈消息处理 | interception.py |
| 自动化测试 | /auto-tests | 规则翻译、测试用例执行 | auto_tests.py |
配置与热重载
这一类 API 管理系统运行时的核心配置,支持编辑后热重载,无需重启服务即可生效。
主 Agent 配置 (/agents)
管理 data/team/main.toml 单一配置文件。当前架构采用"单主 Agent + 动态 Skills"模式,不再维护子 Agent 定义。
| 端点 | 方法 | 说明 |
|---|---|---|
/agents | GET | 列出主 Agent 配置信息、可用 tools/skills、最近重载时间 |
/agents/content/{agent_id} | GET | 读取主 Agent TOML 原始内容(仅支持 __main_team__) |
/agents/content/{agent_id} | PUT | 更新 TOML 文件(自动校验 TOML 语法) |
/agents/reload | POST | 热重载主 Agent 与所有 Skills |
重载操作会同步调用 app.agent.reload_agents(),返回重载耗时(毫秒)。API 不接受任意 agent_id,仅支持固定的虚拟 ID __main_team__。
Sources: agents.py
技能管理 (/skills)
管理 data/skills/*/skill.toml 下的技能定义,支持完整的 CRUD 和热重载生命周期。
| 端点 | 方法 | 说明 |
|---|---|---|
/skills | GET | 列出所有 Skill(含已加载和未加载),展示 tools、scripts、references |
/skills/example | GET | 获取示例 skill.toml 模板 |
/skills/content/{skill_id} | GET | 读取指定 Skill 的 TOML 内容 |
/skills/content/{skill_id} | PUT | 更新 Skill TOML |
/skills | POST | 创建新 Skill(传入 skill_id 查询参数 + TOML 内容体) |
/skills/{skill_id} | DELETE | 删除 Skill TOML 文件 |
/skills/reload | POST | 热重载所有 Skill 定义 |
Skill ID 支持连字符和下划线两种格式,查找时会自动尝试两种变体。
Sources: skills.py
运行时配置 (/settings)
查看和更新运行时配置项。响应中敏感字段(如 wechat_secret、admin_secret_key、数据库连接串等)会被脱敏为 ****。
| 端点 | 方法 | 说明 |
|---|---|---|
/settings | GET | 获取当前配置(含模型角色映射、Agent 参数、可变字段列表) |
/settings | PUT | 更新可变配置项 |
可变字段(无需重启):mem0_enabled、mem0_search_limit、litellm_redis_cache_enabled、litellm_redis_ttl、wechat_miniprogram_title、wechat_miniprogram_pagepath。
需重启字段(更新后标记提示):litellm_redis_cache_enabled、litellm_redis_ttl。
其他字段不允许通过 API 更新,返回 422。
Sources: settings.py
工具描述管理 (/tool-descriptions)
管理 data/tools/descriptions.toml 文件,控制每个工具面向 LLM 的描述文案。
| 端点 | 方法 | 说明 |
|---|---|---|
/tool-descriptions | GET | 获取文件元信息(大小、修改时间、工具列表) |
/tool-descriptions/content | GET | 读取 TOML 内容 |
/tool-descriptions/content | PUT | 更新 TOML 内容(自动校验语法) |
/tool-descriptions/reload | POST | 热重载工具描述到运行时 |
/tool-descriptions/history | GET | 获取历史版本列表 |
/tool-descriptions/history/{version} | GET | 读取指定历史版本内容 |
/tool-descriptions/rollback | POST | 回滚到指定版本 |
每次更新会自动备份旧版本,最多保留 10 个历史版本。编辑后需调用 /reload 才能使运行时生效。
Sources: tool_descriptions.py
灰度配置 (/gray)
管理基于数据库的灰度规则,支持按用户 ID、表达式进行提示词、JSON、文本、模型、Agent 和 Skill 的灰度覆盖。
| 端点 | 方法 | 说明 |
|---|---|---|
/gray/rules | GET | 列出灰度规则(支持按 namespace/path/lang/status 过滤) |
/gray/query | GET | 按 scope 查询(支持 resource_id 快速定位) |
/gray/rules/{rule_id} | GET | 获取单条规则详情 |
/gray/rules | POST | 创建灰度规则 |
/gray/rules/{rule_id} | PUT | 更新灰度规则 |
/gray/rules/{rule_id} | DELETE | 删除灰度规则 |
/gray/preview | POST | 按用户预览资源灰度命中结果 |
/gray/model-preview | POST | 按用户预览模型灰度命中结果 |
/gray/cache/refresh | POST | 刷新灰度运行时缓存 |
灰度规则支持的 namespace:prompt、json、text、model、agent、skill。users_expr 字段支持 *(全部用户)或用户 ID 表达式。merge_mode 支持 replace、json_merge、dict_merge 三种合并模式。
Sources: gray.py
资源管理
资源中心 (/prompts)
自动发现 outfit_agent/data 和 outfit_biz/data 两个数据根目录下的所有文件,按分类展示,并支持文本资源的编辑和版本管理。该模块是 Dashboard "资源中心"页面的核心 API 层。
资源分类体系:
| 分类(Section) | 包含内容 | 排序权重 |
|---|---|---|
| Agent / Team | agents/、team/ 目录下的文件 | 0 |
| Skill | skills/ 目录下的文件 | 1 |
| Prompt | prompts/ 目录下的各类提示词 | 2 |
| Channel | channels/ 目录下的渠道文本 | 3 |
| I18n | i18n/ 目录下的多语言文案 | 4 |
| Tool | tools/ 目录下的工具资源 | 5 |
| Asset | assets/、fonts/ 目录下的素材 | 6 |
| Other | 其他文件 | 7 |
| 端点 | 方法 | 说明 |
|---|---|---|
/prompts/list | GET | 列出所有资源文件(含 overlay 状态、可编辑标记) |
/prompts/content | GET | 读取资源内容或元信息(支持 id 和兼容旧版 path) |
/prompts/content | PUT | 更新资源文件内容(自动备份旧版本) |
/prompts/clear-cache | POST | 清除 PromptLoader / i18n 文本缓存 |
/prompts/history | GET | 获取资源版本历史列表 |
/prompts/history/content | GET | 读取指定历史版本内容 |
/prompts/rollback | POST | 回滚资源到指定版本 |
资源 ID 格式为 source:path(如 agent:prompts/agents/main_team.zh.txt、biz:i18n/system.zh.json)。当 outfit_agent 和 outfit_biz 中存在同名文件时,agent 层标记为 overrides(覆盖),biz 层标记为 shadowed(被覆盖),实现了类似 overlay 文件系统的语义。
更新后 API 返回 recommended_action 字段,提示前端应执行的热应用操作(如 reload_agents、reload_skills、reload_tool_descriptions)。
Sources: prompts.py
统一文件管理 (/content-files)
与 /prompts 类似但更通用的资源管理接口,额外支持二进制文件替换、原始文件下载和显式热应用动作。
| 端点 | 方法 | 说明 |
|---|---|---|
/content-files/list | GET | 列出 data 目录下所有可管理文件(按分组排序) |
/content-files/content | GET | 读取文件内容或元信息 |
/content-files/content | PUT | 更新文本文件内容 |
/content-files/replace | POST | 替换图片等二进制资源(multipart 上传) |
/content-files/apply | POST | 按文件类型执行热应用动作 |
/content-files/history | GET | 获取文件历史版本 |
/content-files/history/content | GET | 读取指定历史版本内容 |
/content-files/rollback | POST | 回滚文件到历史版本 |
/content-files/raw | GET | 原始文件下载(返回 FileResponse) |
文件按 group(如 runtime、prompt、i18n、channel、tool、asset)和 family(如 Agent 定义、Skill 定义、Agent Prompt)两级分类。
Sources: content_files.py
链路追踪与调试
Trace 检查器 (/traces)
直接查询 agno schema 下的 session/trace/span 表,提供 Agent 执行链路的完整可观测性。
| 端点 | 方法 | 说明 |
|---|---|---|
/traces/sessions | GET | 分页列出 sessions(支持 user_id/session_id/agent_id/team_id/status 过滤) |
/traces/sessions/{session_id} | GET | 获取 session 详情(含所有 runs 摘要、dialog_state 快照) |
/traces/runs/{run_id} | GET | 获取单次 run 完整 span 树(含 LLM/工具/Agent 详情、token 统计) |
/traces/live-runs | GET | 获取当前进程内正在运行的 agent run 列表 |
/traces/live-runs/{run_id} | GET | 获取当前进程内单个实时 run 详情 |
/traces/runs/{run_id}/retry-image-generation | POST | 重新入队指定 run 的生图任务 |
Session 详情中包含 dialog_state 的关键字段:active_skill、loaded_skills、stage、sop_stage、pending_confirmation 等,这些字段存储在 agno_sessions.session_data.session_state.dialog_state 路径下。
Run 详情的 span 树按 span_kind 分类展示,包含 LLM(大模型调用)、TOOL(工具执行)、AGENT(Agent 路由)等类型,每个 span 的 attributes 字段携带 OpenInference 标准的可观测性属性。
Sources: traces.py
系统监控 (/monitor)
| 端点 | 方法 | 说明 |
|---|---|---|
/monitor/status | GET | 各服务健康状态(PostgreSQL、Redis、OSS、Mem0、Agent)+ 运行时信息 |
/monitor/stats | GET | 今日统计(请求数、平均响应时间、工具分布、Agent 路由分布) |
/monitor/logs | GET | 最近错误/警告日志(内存环形缓冲区,最多 500 条) |
健康检查通过异步并发执行 5 项检测(_check_database、_check_redis、_check_oss、_check_mem0、_check_agent),所有检查通过时顶层 ok 为 true。
统计信息基于 agno.agno_traces 和 agno.agno_spans 表的当日数据聚合,按 Agent/Team 和工具名称分别展示分布。
Sources: monitor.py
Playground 调试 (/playground)
在管理后台直接模拟 Agent 对话,用于调试 Prompt 效果和验证配置变更。
| 端点 | 方法 | 说明 |
|---|---|---|
/playground/config | GET | 获取 playground 默认配置与当前 Prompt |
/playground/import-history | POST | 按 session_id 导入已有历史消息 |
/playground/run | POST | 运行调试对话(支持 Prompt 覆盖、渠道切换、历史注入) |
Playground 运行时创建独立的 trace session(前缀 playground:),支持覆盖主 Prompt 和渠道 Prompt,可用于 A/B 对比不同 Prompt 版本的效果。channel 参数支持 wechat_miniapp 和 wechat_kefu 两种渠道模式。
Sources: playground.py
用户与数据运维
用户管理 (/users)
| 端点 | 方法 | 说明 |
|---|---|---|
/users/summary | GET | 用户管理总览(总用户数、活跃用户、按渠道分布、数据完整性检查) |
/users/{user_id}/oauth/{oauth_id} | DELETE | 删除用户单条绑定账号 |
| (更多端点) | — | 用户详情、画像查看、按范围数据清理等 |
数据清理支持按 scope 粒度控制:profile(用户画像)、oauth(绑定账号)、items(单品/衣橱)、outfits(穿搭记录)、images(用户图片/头像)、conversations(会话与消息)、insights(离线洞察)、memories(Mem0 记忆)、sessions(Agno 会话)、bind_codes(绑定码)、user(用户主记录,会自动扩展为全量清理)。
Sources: users.py
数据管理 (/data)
| 端点 | 方法 | 说明 |
|---|---|---|
/data/overview | GET | 数据管理总览(单品/穿搭/图片统计与质量检查) |
| (更多端点) | — | 单品列表与详情、穿搭列表与详情、图片列表与详情、向量刷新、文本/图片搜索 |
数据概览包含质量检查指标:缺失向量、缺失描述、归属不匹配、孤儿记录等。单品和图片支持文本搜索和以图搜图两种检索方式。
Sources: data.py
记忆管理 (/memories)
| 端点 | 方法 | 说明 |
|---|---|---|
/memories | GET | 分页列出 Mem0 记忆(支持 q/user_id/run_id/category 过滤) |
/memories/summary | GET | 记忆摘要(按分类统计) |
| (更多端点) | — | 记忆编辑/删除等 |
记忆按分类标签过滤,支持的 category 包括:外观偏好、风格偏好、场景偏好、体型信息、品牌偏好、色彩偏好、禁忌/敏感、对话风格等。
Sources: memories.py
用户洞察 (/insights)
| 端点 | 方法 | 说明 |
|---|---|---|
/insights/summary | GET | 洞察摘要(总记录数、用户数、按分类统计) |
/insights | GET | 分页查询洞察历史(支持 user_id/category/q 过滤) |
/insights/users/{user_id}/latest | GET | 获取指定用户各分类的最新洞察快照 |
洞察数据由离线调度任务(insight_scheduler_cron,默认每日零点)自动生成,存储在 user_insights 表中。
Sources: insights.py
会话消息 (/message-management)
| 端点 | 方法 | 说明 |
|---|---|---|
/message-management/sessions | GET | 会话消息列表(支持 channel/q 过滤,含最新消息预览) |
/message-management/sessions/{conversation_id}/messages | GET | 会话消息详情(分页,支持 before_id 前向翻页) |
会话列表的搜索支持按 session_id、用户昵称、标题和最新消息内容进行模糊匹配。
Sources: message_management.py
OSS 文件管理 (/oss)
| 端点 | 方法 | 说明 |
|---|---|---|
/oss/list | GET | 列出 OSS 文件(按 prefix 前缀浏览) |
/oss/signed-url | GET | 获取签名预览 URL |
/oss/object | DELETE | 删除单个文件 |
/oss/batch-delete | POST | 批量删除文件 |
/oss/mkdir | POST | 创建文件夹 |
/oss/upload | POST | 上传文件 |
/oss/upload-zip | POST | ZIP 批量上传(自动解压) |
/oss/folder-info | GET | 查询文件夹文件数 |
/oss/folder-delete | POST | 递归删除文件夹 |
/oss/folder-copy | POST | 复制或移动文件夹 |
/oss/copy | POST | 复制或移动单个文件 |
Sources: oss.py
内测统计 (/beta-users)
| 端点 | 方法 | 说明 |
|---|---|---|
/beta-users/dashboard | GET | 内测用户实时统计面板 |
统计面板包含:用户激活时间、使用次数、工作流触发次数、试穿次数、单品上传数、画布进入/生成/修改次数、负面反馈、修改意图分析等指标,支持按 A/B 分组聚合。
Sources: beta_users.py
安全与准入
消息准入控制 (/access-control)
| 端点 | 方法 | 说明 |
|---|---|---|
/access-control/summary | GET | 消息准入控制总览 |
/access-control/policies | GET/POST | 准入策略列表 / 创建策略 |
/access-control/policies/{policy_id} | PUT/DELETE | 更新 / 删除策略 |
/access-control/entries | GET/POST | 黑白名单列表 / 创建条目 |
/access-control/entries/{entry_id} | PUT/DELETE | 更新 / 删除条目 |
/access-control/codes | GET | 授权码列表 |
/access-control/codes/generate | POST | 批量生成授权码 |
/access-control/codes/{code_id}/disable | POST | 停用授权码 |
策略按 channel(渠道)和 service_account(客服账号)维度定义,支持 allowlist(白名单)、blocklist(黑名单)、code(授权码)三种策略类型。匹配类型支持 user_id、unionid、channel + openid。
Sources: access_control.py
消息拦截 (/interception)
| 端点 | 方法 | 说明 |
|---|---|---|
/interception/rules | GET/POST | 拦截规则列表 / 创建规则 |
/interception/rules/{rule_id} | PUT/DELETE | 更新 / 删除规则 |
/interception/feedback | GET | 反馈/人工客服消息列表 |
/interception/feedback/{message_id}/processed | POST | 标记反馈已处理 |
/interception/feedback/{message_id}/reply | POST | 回复反馈消息 |
拦截规则支持精确消息匹配(exact_messages)、通配符匹配(wildcard_patterns)、正则匹配(regex_patterns)和用户画像条件匹配(profile_conditions)。触发后的动作类型包括 auto_reply(自动回复)等。
Sources: interception.py
自动化测试 (/auto-tests)
| 端点 | 方法 | 说明 |
|---|---|---|
/auto-tests/rules/translate | POST | 将自然语言规则描述转换为 JSON 规则 |
/auto-tests/executions | POST | 执行自动化测试用例 |
该模块提供 LLM 辅助的规则翻译能力,将自然语言描述的测试断言转换为结构化的 AutoTestRule JSON,然后可在管理后台直接执行测试用例。
Sources: auto_tests.py
通用设计模式
错误响应格式
所有 Admin API 端点遵循 FastAPI 标准错误格式:
{
"detail": "错误描述信息"
}
常见状态码:401(缺少 Authorization 头)、403(Token 无效或未配置)、404(资源不存在)、422(请求体校验失败或字段不允许更新)、500(服务端内部错误)。
版本历史与回滚
资源管理类端点(/prompts、/content-files、/tool-descriptions)共享相同的版本管理模式:
- 1. 自动备份:每次写入前将当前内容复制到历史目录,文件名使用 Unix 时间戳
- 2. 容量控制:最多保留 10 个历史版本,超出时自动删除最旧版本
- 3. 回滚流程:读取指定版本内容 → 备份当前版本 → 覆盖当前文件 → 清除运行时缓存
缓存清除
编辑配置后需要清除相应的运行时缓存才能生效:
- - 文本缓存:调用
i18n_loader.clear_cache()和clear_instruction_template_cache() - - 工具描述缓存:调用
clear_tool_description_cache()和clear_runtime_tool_cache() - - 灰度缓存:调用灰度服务的运行时缓存失效接口
各管理端点在保存后会根据资源类型自动执行对应的缓存清除操作。
配置管理层级关系
graph TD
A[".env 环境变量"] --> B["AgentSettings (config.py)"]
C["outfit_biz/config.yml"] --> D["深度合并 (deep-merge)"]
E["outfit_agent/config.yml"] --> D
D --> B
B --> F["/api/admin/settings GET"]
F --> G["Dashboard 前端"]
G -->|PUT 可变字段| H["/api/admin/settings PUT"]
H --> I["运行时生效"]
outfit_agent/config.yml 在 outfit_biz/config.yml 之上进行深度合并(_deep_merge),agent 配置优先。Admin API 的 /settings 端点仅暴露有限的可变字段供运行时修改,敏感字段统一脱敏。
<a id="25-lian-lu-zhui-zong-yu-trace"></a>
---
链路追踪与 Trace
- - Section: 管理与运维
- - Group: 监控与调试
- - Source File:
25-lian-lu-zhui-zong-yu-trace.md
本页全面梳理 outfit_agent 系统的链路追踪架构。系统采用双层追踪策略——基于 PostgreSQL 的持久化 Trace(Agno OpenTelemetry)与基于 Redis 的实时 Live Runs——二者互补,共同覆盖"历史回溯"和"实时观测"两个核心运维场景。追踪数据贯穿 Session → Run → Span → Tool Call 完整生命周期,并在 Dashboard 的三个页面中以树形结构可视化呈现。
整体架构
系统追踪架构由四层组成:数据采集层(Agno 框架 OTel 自动注入 + 自定义 Tool Hook)、存储层(PostgreSQL agno schema 持久化 + Redis 实时缓存)、查询聚合层(Admin API)、展示层(Dashboard 前端)。
graph TB
subgraph 数据采集层
A1[Agno Agent arun] -->|OpenTelemetry| B1[AgnoInstrumentor]
A1 -->|tool_hook| B2[live_run_tool_hook]
A2[LoggedOpenAIChat] -->|logger.info| B3[应用日志]
B3 -->|OTLP HTTP| B4[HyperDX]
end
subgraph 存储层
B1 -->|DatabaseSpanExporter| C1[(PostgreSQL agno schema)]
C1 --> C1a[agno_sessions]
C1 --> C1b[agno_traces]
C1 --> C1c[agno_spans]
B2 -->|Redis setex| C2[(Redis)]
C2 --> C2a[live_runs:active 集合]
C2 --> C2b[live_runs:run:{id} 键]
end
subgraph 查询聚合层
D1[traces.py Admin API] -->|SQL| C1
D1 -->|Redis get| C2
end
subgraph 展示层
E1[TraceInspector] -->|HTTP| D1
E2[TraceSessionDetail] -->|HTTP| D1
E3[TraceRunDetail] -->|HTTP| D1
end
数据采集触发点:当 Agent 执行 arun() 时,Agno 框架自动在 OpenTelemetry TracerProvider 下创建 Trace 与 Span 并写入 agno_spans;同时 _make_agent_tool_hooks() 注册的 live_run_tool_hook 会在每次工具调用前后将进度写入 Redis。两条路径独立运行,互不阻塞。
Sources: agentos.py, agent.py, live_runs.py
持久化 Trace 存储:Agno PostgreSQL Schema
持久化追踪数据存储在 PostgreSQL 的 agno schema 下,由三张核心表构成。Agno 框架在 Agent 执行期间通过 OpenTelemetry SDK + DatabaseSpanExporter 自动完成数据写入,开发者无需手动插入。
erDiagram
agno_sessions ||--o{ agno_traces : "session_id"
agno_traces ||--o{ agno_spans : "trace_id"
agno_sessions {
uuid session_id PK
text user_id
jsonb session_data "包含 session_state(dialog_state / tool_ctx)"
jsonb runs "历史 RunOutput 数组"
timestamp created_at
timestamp updated_at
}
agno_traces {
uuid trace_id PK
text run_id
text session_id FK
text agent_id
text team_id
text user_id
text status "OK | ERROR"
text name
timestamp start_time
timestamp end_time
bigint duration_ms
}
agno_spans {
uuid span_id PK
uuid parent_span_id "自引用,构建 span 树"
uuid trace_id FK
text name
text span_kind "LLM | TOOL | AGENT | CHAIN | INTERNAL"
text status_code
text status_message
timestamp start_time
timestamp end_time
bigint duration_ms
jsonb attributes "OpenInference 标准属性"
}
agno_sessions 是会话级容器,runs 字段以 JSONB 数组存储每次 Agent 调用的完整 RunOutput(包含 messages、tools、metrics)。session_data 则持久化了 session_state,其中包括业务层关心的 dialog_state(SOP 阶段、活跃技能、穿搭任务状态等)和 tool_ctx(渠道、语言、用户 ID 等上下文)。
agno_traces 是 Run 级追踪记录,一条 trace 对应一次 Agent arun() 调用。run_id 将 trace 与 session runs 中的 RunOutput 关联。当 trace 来自 Team 协作场景时,team_id 字段标识委派来源。
agno_spans 是最小追踪单元,通过 parent_span_id 自引用构建有向无环图(DAG),形成 span 树。span_kind 标识 span 类型,attributes 以 JSONB 存储 OpenInference 标准属性(如 llm.model_name、tool.parameters、tool.name 等),供聚合层按类型提取元数据。
Sources: traces.py, traces.py, traces.py
Span 类型推导
Admin API 在查询 span 树时,通过 _derive_span_type() 函数将原始 span_kind 和 attributes 推导为统一的业务类型标签。这一推导逻辑优先使用 OpenInference 标准,然后回退到 agno 原始 kind,最后通过 attributes 特征识别。
| 推导类型 | 判断条件 | 典型场景 |
|---|---|---|
| LLM | openinference.span.kind == "CHAIN" 不适用时,llm.model_name 存在或 span 名以 .ainvoke 结尾 | 模型推理调用 |
| TOOL | tool.name 或 tool.parameters 存在于 attributes | 工具/函数调用 |
| AGENT | openinference.span.kind == "AGENT" 且无 agno.team.id;或 agno.agent.id / agent.name 存在 | 单 Agent 执行 |
| TEAM | openinference.span.kind == "AGENT" 且 agno.team.id 存在;或 attributes 中有 agno.team.id / team.id | Team 协作委派 |
| WORKFLOW | openinference.span.kind == "CHAIN" | 工作流编排(如穿搭生成流程) |
每种类型的 span 在聚合时会提取不同的元数据字段:LLM 类型提取 model、provider、input_tokens、output_tokens;TOOL 类型提取 tool_name、parameters、output;AGENT/TEAM 类型提取 agent_id、team_id、run_id、session_id。此外,所有类型的 span 都会扫描 workflow.、outfit.、dialog.、skill.、sop.* 前缀的 attributes 并提取为额外元数据。
实时追踪:Live Runs(Redis)
持久化 Trace 在 Agent 运行完成后才有完整数据,而 Live Runs 机制在 Agent 执行期间就提供实时可观测性。它以 Redis 为存储后端,以 TTL + 心跳实现生命周期管理。
sequenceDiagram
participant Inbound as 消息入站
participant Agent as Agent arun
participant Redis as Redis
participant ToolHook as live_run_tool_hook
participant Dashboard as Dashboard
Inbound->>Agent: 用户消息
Agent->>Redis: register_live_run(run_id, session_state)
Note over Redis: SET agent:live_runs:run:{id}<br/>SADD agent:live_runs:active {id}
Agent->>ToolHook: 工具调用前
ToolHook->>Redis: _start_tool_call(name, args)
Note over Redis: 追加 tool_calls 条目<br/>status=running
Agent->>ToolHook: 工具调用完成
ToolHook->>Redis: _finish_tool_call(result/error)
Note over Redis: 更新 status=completed/error<br/>记录 result_preview
loop 心跳 (每 15 秒)
Agent->>Redis: _refresh_payload_ttl(run_id)
end
Dashboard->>Redis: list_live_run_summaries()
Redis-->>Dashboard: 当前活跃 run 列表
Agent->>Redis: unregister_live_run(run_id)
Note over Redis: DEL run key + SREM active 集合
核心数据结构:每个 Live Run 在 Redis 中对应一个键 agent:live_runs:run:{run_id},其 payload 包含:
- - 身份信息:
run_id、session_id、user_id、agent_id、agent_name、agent_model - - 时间信息:
started_at、updated_at、心跳 TTL - - 输入信息:
raw_message(原始用户消息)、input_chars、memory_context_chars - - 会话状态快照:
tool_ctx(渠道/语言/用户)、dialog_state(SOP 阶段/活跃技能/穿搭状态) - - 工具调用列表:
tool_calls数组,每个条目包含call_id、name、status、args、result_preview、started_at、ended_at - - Worker 信息:
worker_id(hostname:pid)
工具钩子注册:live_run_tool_hook 通过 _make_agent_tool_hooks() 注册为 Agent 的 tool_hooks。Agno 框架在每次工具调用时自动触发该钩子,钩子在调用前后分别执行 _start_tool_call 和 _finish_tool_call,实现零侵入的工具级追踪。钩子兼容 Agno 的多种关键字参数(next_func、func、function、function_call),确保与框架版本兼容。
生命周期管理:Live Run 的 Redis 键默认 TTL 为 180 秒(可通过 live_run_redis_ttl_seconds 配置),心跳间隔 15 秒(live_run_heartbeat_interval_seconds)。心跳任务通过 start_live_run_heartbeat() 启动为异步 Task,在 Agent run 结束时通过 stop_live_run_heartbeat() 取消。若 run 异常中断导致心跳停止,TTL 自然过期后数据自动清除,避免 Redis 中残留过期数据。
Sources: live_runs.py, agent.py, config.yml
LLM 调用日志
LoggedOpenAIChat 继承 Agno 的 OpenAIChat,通过重写 ainvoke() 和 ainvoke_stream() 方法,在每次 LLM 请求前后记录结构化日志。这是一条独立于 OTel Trace 的日志通路,通过标准 Python logging 输出,再由 app/observability.py 中配置的 OTLP Log Exporter 转发至 HyperDX。
| 日志阶段 | 格式标识 | 记录内容 |
|---|---|---|
| 请求开始 | [llm:async:start] | owner、run_id、session_id、model、base_url、消息数量、payload 字节数、content 字符数、角色分布、工具数量、response_format、tool_choice、HTTP 客户端配置 |
| 请求完成 | [llm:async:done] | owner、run_id、session_id、model、耗时(ms)、响应字符数 |
| 请求失败 | [llm:async:error] | 以上全部 + error_type、error 详情、payload 字节数 |
除了日志,LoggedOpenAIChat 还修正了 Agno 框架的一个时间戳问题:ModelResponse.created_at 默认是类级别的 int(time()) 值(在 import 时冻结),_stamp_model_response_now() 将其修正为实例创建时的真实时间,避免持久化到 session runs 中的时间戳过期。
Sources: llm_logging.py
HyperDX 日志导出
app/observability.py 负责将应用日志通过 OpenTelemetry OTLP 协议导出至 HyperDX(一个基于 OTel 的日志分析平台)。该模块仅导出 logs(不含 traces 或 metrics),作为 Agno 持久化 Trace 的补充通道。
| 配置环境变量 | 必填 | 说明 |
|---|---|---|
HYPERDX_API_KEY | 是 | HyperDX 平台 API Key |
OTEL_SERVICE_NAME | 是 | 服务名称标识(如 outfit) |
HYPERDX_API_ENDPOINT | 否 | 自定义端点,默认 https://in-otel.hyperdx.io |
OTEL_EXPORTER_OTLP_ENDPOINT | 否 | 备用全局 OTLP 端点 |
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT | 否 | 日志专用端点(最高优先级) |
OTEL_SERVICE_VERSION | 否 | 服务版本号 |
初始化流程:init_hyperdx_monitoring() → load_hyperdx_config() 读取环境变量 → _ensure_log_exporter() 创建 LoggerProvider + BatchLogRecordProcessor + OTLPLogExporter → 注册 LoggingHandler 到 root logger。模块通过全局 _log_exporter_installed 标志保证幂等,重复调用不会创建多个 exporter。
Sources: observability.py, .env.example
Admin API 端点
app/routers/admin/traces.py 提供完整的追踪数据查询接口,所有端点均需要 Admin 鉴权(AdminRequired 依赖注入)。
| 端点 | 方法 | 说明 |
|---|---|---|
/traces/sessions | GET | 分页列出 sessions,支持按 user_id、session_id(模糊)、agent_id、team_id、status(error/ok)过滤,返回每个 session 的 trace 统计、渠道信息、dialog_state 摘要 |
/traces/sessions/{session_id} | GET | 单个 session 详情,包含所有 runs 摘要(tool_calls、tokens、消息摘要)、session_state(dialog_state + tool_ctx)完整信息、灰度配置匹配情况 |
/traces/runs/{run_id} | GET | 单次 run 的完整 span 树(支持 include_infra_spans 参数过滤基础设施 span)、LLM token 统计、工具调用详情、消息时间线、委派步骤、图片生成上下文、关联 run 链 |
/traces/live-runs | GET | 当前 Redis 中活跃的 Live Run 列表,支持按 session_id 过滤 |
/traces/live-runs/{run_id} | GET | 单个 Live Run 详情,包含完整 tool_calls 列表、session_state 快照 |
/traces/runs/{run_id}/retry-image-generation | POST | 重新入队指定 run 的生图任务,支持 reset_health 参数清空 upstream 健康惩罚 |
查询策略:/traces/runs/{run_id} 端点采用双轨查询策略——首先在 agno_traces 中按 run_id 或 trace_id 查找持久化 trace;若未命中(例如 trace 尚未写入或来自非 OTel 通路),则回退到 agno_sessions.runs JSONB 数组中按 run_id 匹配 RunOutput payload,从中合成 span 树。这种设计确保无论数据来自哪条通路,都能返回完整结果。
关联 Run 发现:查询 run detail 时,系统会递归扫描 span 树的 input/output/meta 字段,提取所有 run_id、child_run_id、workflow_run_id、outfit_run_id 等关联标识,然后递归加载关联 run 的 span 树并挂载到当前 span 树的对应工具节点下。这使得跨 run 的工作流(如穿搭生成 → 图片生成)能在一棵 span 树中完整呈现。
Sources: traces.py, traces.py, traces.py
Dashboard 前端展示
Dashboard 提供三个页面构成追踪数据的可视化链路:
TraceInspector(会话列表 + 实时运行):入口页面,左侧展示分页 session 列表(带 trace 统计、渠道信息、最近消息预览),右侧展示当前 Redis 中的 Live Runs(带工具调用进度、SOP 阶段状态)。Live Runs 区域自动刷新,可直接点击查看实时详情。
TraceSessionDetail(会话详情):展示单个 session 的完整历史,包括所有 runs 的时间线、每次 run 的工具调用摘要、token 统计、dialog_state 变化。可从某个 run 跳转到 TraceRunDetail。
TraceRunDetail(Run 详情):最核心的排障页面,以树形结构渲染 span 树。每个 span 节点显示类型标签(AGENT/TEAM/LLM/TOOL/WORKFLOW)、状态(OK/ERROR/RUNNING)、耗时、元数据。LLM span 展示模型、token 用量;TOOL span 展示参数和输出;AGENT/TEAM span 展示委派详情。关联 run 的 span 以嵌入方式挂载在对应工具节点下。页面同时展示用户消息、Agent 回复、消息时间线、图片生成上下文等业务信息。
Sources: TraceInspector.tsx, TraceRunDetail.tsx, traces.ts
追踪数据与业务关联
追踪系统不仅记录技术调用链路,还深度嵌入业务语义。Session 级别的 dialog_state 被完整持久化并在 trace 查询中暴露,使得排障时可以直接看到:
- - 当前活跃技能(
active_skill):哪个 skill 正在处理用户请求 - - SOP 阶段(
sop_stage):流程推进到了哪一步(如await_search_confirmation) - - 待确认状态(
pending_confirmation):是否在等待用户确认 - - 穿搭任务状态(
outfit_status/outfit_run_id):穿搭生成是否在进行中 - - 构想摘要(
idea_summary):用户的穿搭构想 - - 灰度配置(
gray_summary):当前用户命中的灰度规则(prompt 和 model 维度)
这种设计使得"排查问题时优先看 dialog_state"成为团队的核心排障策略——从 session 列表出发,快速定位到异常 session,再逐层深入到 run → span → tool call,形成完整的排障链路。
Sources: traces.py, docs/dashboard.md
追踪系统初始化流程
追踪系统的初始化分为两条路径,在应用启动时分别执行:
flowchart TD
A[app/main.py lifespan] --> B[init_agent]
B --> C[构建 Agent<br/>注册 tool_hooks]
A --> D[build_agent_os]
D --> E[build_agno_db<br/>PostgresDb schema=agno]
D --> F[ensure_agno_db_tracing]
F --> G[TracerProvider + DatabaseSpanProcessor]
F --> H[AgnoInstrumentor.instrument]
D --> I[AgentOS tracing=True]
C --> J[live_run_tool_hook 已注册]
I --> K[OTel Trace 自动采集就绪]
J --> L{Agent arun 执行}
K --> L
L --> M[Redis Live Run 写入]
L --> N[OTel Span 持久化到 PostgreSQL]
_TaskSafePostgresDb 是对 Agno 的 PostgresDb 的关键适配:Agno 默认使用 scoped_session(线程本地),但在 FastAPI/asyncio 环境下,多个 trace export 回调可能在同一线程运行导致 SQLAlchemy 状态冲突。_TaskSafePostgresDb 将 scoped_session 替换为普通 sessionmaker,确保每次导出调用获得独立的 Session。
Sources: agentos.py, agentos.py, agent.py
配置项
| 配置项 | 默认值 | 说明 |
|---|---|---|
live_run_redis_ttl_seconds | 180 | Live Run Redis 键 TTL(秒) |
live_run_heartbeat_interval_seconds | 15 | Live Run 心跳刷新间隔(秒) |
HYPERDX_API_KEY | 空 | HyperDX 日志导出 API Key |
OTEL_SERVICE_NAME | 空 | OTel 服务名称 |
HYPERDX_API_ENDPOINT | https://in-otel.hyperdx.io | HyperDX 端点 |
<a id="26-xi-tong-jian-kong-yu-jian-kang-jian-cha"></a>
---
系统监控与健康检查
- - Section: 管理与运维
- - Group: 监控与调试
- - Source File:
26-xi-tong-jian-kong-yu-jian-kang-jian-cha.md
本文档详细介绍了 Outfit Agent 系统的监控体系与健康检查机制,包括实时服务状态监控、性能统计、日志收集以及链路追踪等核心功能。系统采用多层次监控架构,从简单的健康检查端点到完整的分布式链路追踪,为运维人员和开发者提供全面的系统可观测性。
监控架构概览
Outfit Agent 的监控系统采用分层架构设计,包含三个主要监控层次:
graph TB
subgraph "前端监控层"
A[Dashboard 监控面板]
end
subgraph "API 监控层"
B[健康检查端点 /health]
C[Admin 监控 API /api/admin/monitor/*]
D[链路追踪 API /api/admin/traces/*]
end
subgraph "数据收集层"
E[OpenTelemetry 日志导出]
F[Redis 实时运行监控]
G[数据库 Trace 存储]
H[内存日志缓冲区]
end
subgraph "外部服务"
I[HyperDX 日志平台]
J[PostgreSQL 数据库]
K[Redis 缓存]
end
A --> C
A --> D
B --> L[负载均衡器/健康检查]
C --> F
C --> H
D --> G
E --> I
F --> K
G --> J
Sources: main.py, monitor.py, observability.py
健康检查端点
系统提供基础的健康检查端点,用于外部监控系统(如负载均衡器、容器编排平台)快速验证服务可用性。
端点详情
| 端点 | 方法 | 认证要求 | 响应格式 |
|---|---|---|---|
/health | GET | 无 | {"status": "ok"} |
健康检查端点设计为轻量级、无状态的响应,不依赖任何外部服务,确保在系统部分故障时仍能返回成功状态。这对于容器编排平台的存活探针(liveness probe)至关重要。
Sources: main.py
Admin 监控 API
Admin 监控 API 提供系统运行状态的详细视图,包括服务健康状态、性能统计和日志监控。所有端点都需要管理员令牌认证。
服务状态监控
GET /api/admin/monitor/status 端点提供全面的服务健康状态检查,包括:
graph LR
A[状态检查请求] --> B[并行检查所有服务]
B --> C[PostgreSQL 数据库]
B --> D[Redis 缓存]
B --> E[OSS 对象存储]
B --> F[Mem0 记忆服务]
B --> G[Agent 主服务]
C --> H[汇总状态响应]
D --> H
E --> H
F --> H
G --> H
系统通过并发检查五个核心服务的连接状态:
- 1. PostgreSQL 数据库:执行
SELECT 1验证连接 - 2. Redis 缓存:执行
PING命令验证连接 - 3. OSS 对象存储:检查配置是否完整
- 4. Mem0 记忆服务:验证记忆模块初始化状态
- 5. Agent 主服务:确认主 Agent 是否成功加载
响应示例:
{
"ok": true,
"services": {
"postgresql": {"ok": true, "detail": "connected"},
"redis": {"ok": true, "detail": "connected"},
"oss": {"ok": true, "detail": "configured"},
"mem0": {"ok": true, "detail": "enabled"},
"agent": {"ok": true, "detail": "main agent loaded: agent-id-123"}
},
"started_at": "2026-04-15T09:30:00+00:00",
"uptime_seconds": 125
}
Sources: monitor.py
性能统计监控
GET /api/admin/monitor/stats 端点提供今日运行统计信息,包括:
| 统计指标 | 说明 | 数据来源 |
|---|---|---|
total_runs | 今日总请求数 | agno.agno_traces 表 |
avg_response_s | 平均响应时间(秒) | traces.duration_ms 计算 |
tool_distribution | 工具调用分布(Top 20) | agno.agno_spans 表 |
agent_distribution | Agent 路由分布 | traces.agent_id/team_id |
统计查询基于 Agno 框架的追踪数据,通过 SQL 聚合计算得出。响应时间计算考虑了所有已完成的 trace 记录,工具分布统计则按调用次数降序排列。
Sources: monitor.py
日志监控
GET /api/admin/monitor/logs 端点提供最近错误和警告日志的实时查看功能:
graph TB
A[日志请求] --> B[内存环形缓冲区]
B --> C[按级别过滤]
C --> D[按时间倒序排列]
D --> E[返回日志列表]
系统使用内存环形缓冲区存储最近的 500 条日志记录,支持按日志级别(warn/error)过滤。缓冲区采用先进先出策略,确保始终保留最新的日志数据。
日志记录格式:
{
"time": 1713168600,
"level": "ERROR",
"logger": "app.services.outfit_workflow_service",
"message": "Workflow execution failed: timeout after 30s"
}
Sources: monitor.py
实时运行监控
系统通过 Redis 实现实时运行监控,追踪当前正在执行的 Agent 运行任务。
Live Runs 监控机制
sequenceDiagram
participant Agent as Agent 运行时
participant Redis as Redis 存储
participant Admin as Admin API
Agent->>Redis: 注册运行任务 (register_live_run)
loop 心跳更新
Agent->>Redis: 刷新 TTL (start_live_run_heartbeat)
end
Agent->>Redis: 记录工具调用 (_start_tool_call)
Agent->>Redis: 更新工具状态 (_finish_tool_call)
Admin->>Redis: 查询活跃运行 (list_live_run_summaries)
Admin->>Redis: 获取运行详情 (get_live_run_detail)
Agent->>Redis: 注销运行任务 (unregister_live_run)
实时监控系统提供以下功能:
- 1. 运行任务注册:Agent 开始执行时注册运行信息到 Redis
- 2. 心跳保活:定期刷新 Redis 键的 TTL,防止任务过期
- 3. 工具调用追踪:记录每个工具调用的开始、结束时间和结果
- 4. 状态快照:保存运行时的会话状态、对话状态和工具上下文
监控数据结构:
- -
agent:live_runs:active:活跃运行 ID 集合 - -
agent:live_runs:run:{run_id}:单个运行的详细信息
Sources: live_runs.py
实时监控 API
| 端点 | 方法 | 说明 |
|---|---|---|
/api/admin/traces/live-runs | GET | 获取当前活跃的运行列表 |
/api/admin/traces/live-runs/{run_id} | GET | 获取单个运行的详细信息 |
实时监控 API 支持按 session_id 过滤,返回运行任务的详细状态,包括:
- - 运行基本信息(run_id, session_id, user_id)
- - Agent 信息(agent_id, agent_name, agent_model)
- - 时间信息(started_at, updated_at, elapsed_ms)
- - 工具调用状态(active_tool_names, tool_call_count)
- - 对话状态(active_skill, sop_stage, outfit_status)
Sources: traces.py
链路追踪系统
系统提供完整的链路追踪功能,基于 Agno 框架的追踪数据,支持多维度查询和分析。
追踪数据模型
erDiagram
agno_sessions ||--o{ agno_traces : "包含"
agno_traces ||--o{ agno_spans : "包含"
agno_sessions {
uuid session_id PK
varchar user_id
timestamp created_at
jsonb session_data
}
agno_traces {
uuid trace_id PK
uuid session_id FK
varchar agent_id
varchar team_id
varchar status
integer duration_ms
timestamp start_time
}
agno_spans {
uuid span_id PK
uuid trace_id FK
varchar name
varchar span_kind
jsonb attributes
timestamp start_time
integer duration_ms
}
追踪系统提供三个主要查询接口:
- 1. 会话列表查询:支持按用户 ID、会话 ID、Agent ID、团队 ID 和状态过滤
- 2. 会话详情查询:获取单个会话的所有运行记录摘要
- 3. 运行详情查询:获取单次运行的完整 span 树,包括 LLM 调用、工具调用和 Agent 路由详情
Sources: traces.py
追踪查询 API
| 端点 | 方法 | 说明 | 过滤参数 |
|---|---|---|---|
/api/admin/traces/sessions | GET | 分页列出会话 | user_id, session_id, agent_id, team_id, status |
/api/admin/traces/sessions/{session_id} | GET | 会话详情 | - |
/api/admin/traces/runs/{run_id} | GET | 运行详情 | include_infra_spans |
运行详情查询返回完整的 span 树结构,包括:
- - LLM 调用详情:模型名称、token 使用、响应时间
- - 工具调用详情:工具名称、参数、执行结果、耗时
- - Agent 路由详情:子 Agent 调用、委派决策
- - 基础设施 span:HTTP 请求、数据库查询(可选显示)
Sources: traces.py
外部可观测性集成
系统支持与 HyperDX 日志平台的集成,通过 OpenTelemetry 标准协议导出日志数据。
HyperDX 集成架构
graph LR
A[应用日志] --> B[OpenTelemetry SDK]
B --> C[OTLP HTTP 导出器]
C --> D[HyperDX 平台]
subgraph "配置参数"
E[HYPERDX_API_KEY]
F[OTEL_SERVICE_NAME]
G[OTEL_EXPORTER_OTLP_ENDPOINT]
end
E --> C
F --> B
G --> C
集成配置通过环境变量控制:
| 环境变量 | 说明 | 默认值 |
|---|---|---|
HYPERDX_API_KEY | HyperDX API 密钥 | 空(禁用集成) |
OTEL_SERVICE_NAME | 服务名称 | 空(禁用集成) |
OTEL_EXPORTER_OTLP_ENDPOINT | OTLP 端点 | https://in-otel.hyperdx.io |
OTEL_SERVICE_VERSION | 服务版本 | 空 |
日志导出配置:
- - 使用
BatchLogRecordProcessor批量处理日志 - - 通过 HTTP/HTTPS 协议发送到 OTLP 端点
- - 支持自定义日志端点覆盖
Sources: observability.py
Dashboard 监控面板
Dashboard 提供可视化的监控界面,集成所有监控数据源,提供实时系统状态概览。
监控面板功能
Dashboard 监控面板包含以下组件:
- 1. 服务状态卡片:显示所有核心服务的健康状态
- 2. 今日统计概览:请求总数、平均响应时间、工具调用种类
- 3. 工具调用分布图:Top 10 工具调用的柱状图
- 4. Agent 路由分布图:Agent 调用的饼图分布
- 5. 错误日志表格:最近错误和警告日志的实时查看
面板数据每 30 秒自动刷新,支持手动刷新操作。所有数据通过 Admin API 获取,确保数据的一致性和实时性。
Sources: Monitor.tsx, monitor.ts
配置参数
监控系统相关配置参数在 config.yml 中定义:
# 实时运行监控配置
live_run_redis_ttl_seconds: 180 # 运行任务 Redis TTL(秒)
live_run_heartbeat_interval_seconds: 15 # 心跳间隔(秒)
# HyperDX 集成配置(通过环境变量设置)
# HYPERDX_API_KEY: your-api-key
# OTEL_SERVICE_NAME: outfit-agent
# OTEL_EXPORTER_OTLP_ENDPOINT: https://in-otel.hyperdx.io
Sources: config.py, config.yml
监控最佳实践
健康检查配置
对于生产环境部署,建议配置以下健康检查策略:
- 1. 存活探针(Liveness Probe):使用
/health端点
- - 检查间隔:10 秒
- - 超时时间:5 秒
- - 失败阈值:3 次
- 2. 就绪探针(Readiness Probe):使用
/api/admin/monitor/status端点
- - 检查间隔:30 秒
- - 超时时间:10 秒
- - 失败阈值:3 次
告警配置建议
基于监控数据,建议配置以下告警规则:
| 告警类型 | 触发条件 | 告警级别 |
|---|---|---|
| 服务不可用 | 任一核心服务状态检查失败 | 严重 |
| 响应时间过长 | 平均响应时间 > 10 秒 | 警告 |
| 错误率过高 | 最近 5 分钟错误日志 > 10 条 | 严重 |
| 内存使用过高 | 进程内存使用 > 80% | 警告 |
日志监控策略
- 1. 日志级别:生产环境建议使用 INFO 级别,开发环境使用 DEBUG 级别
- 2. 日志轮转:配置日志文件轮转,避免磁盘空间不足
- 3. 日志聚合:通过 HyperDX 集中收集和分析日志
- 4. 敏感信息:确保日志中不包含用户敏感信息
故障排查指南
常见问题及解决方案
| 问题现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 健康检查失败 | 服务未启动 | 检查服务进程状态,查看启动日志 |
| 监控数据不更新 | Redis 连接问题 | 检查 Redis 连接配置和网络连通性 |
| 追踪数据缺失 | 数据库连接问题 | 检查 PostgreSQL 连接和 agno schema |
| 日志导出失败 | HyperDX 配置错误 | 验证 API 密钥和端点配置 |
监控数据验证
定期验证监控数据的完整性:
- 1. 健康检查验证:确认所有服务状态检查通过
- 2. 统计数据验证:对比数据库实际记录与统计结果
- 3. 日志完整性:检查错误日志是否正常记录和导出
- 4. 追踪数据:验证追踪记录的完整性和准确性
相关文档
- - 链路追踪与 Trace:详细了解链路追踪系统的使用和配置
- - Dashboard 管理面板:Dashboard 的完整功能和使用指南
- - Admin API 接口:Admin API 的完整接口文档
- - 配置文件结构:系统配置文件的详细说明
<a id="27-dan-yuan-ce-shi-yu-ji-cheng-ce-shi"></a>
---
单元测试与集成测试
- - Section: 开发与测试
- - Source File:
27-dan-yuan-ce-shi-yu-ji-cheng-ce-shi.md
本文档系统介绍 outfit-agent 项目的测试体系架构,涵盖单元测试、集成测试、API 测试以及端到端自动化测试框架的设计与实践。项目基于 pytest + pytest-asyncio 构建测试基础设施,通过分层测试策略保障代码质量与业务逻辑的正确性。
测试框架与配置
项目采用 pytest 作为核心测试框架,配合 pytest-asyncio 实现异步测试支持。测试配置集中定义在 pyproject.toml 中,关键配置包括:asyncio_mode = "auto" 使所有异步测试自动运行,testpaths = ["tests"] 指定测试目录。开发依赖通过 [project.optional-dependencies] 管理,包含 pytest>=8.2.0、pytest-asyncio>=0.23.0、httpx>=0.27.0 等。
Sources: pyproject.toml
测试目录结构清晰,共 98 个测试文件,遵循 test_<module_name>.py 的命名约定,与 app/ 下的源码模块一一对应。此外还有 tests/auto_repair/ 目录用于端到端场景测试,tests/tools/ 目录用于测试辅助工具。
tests/
├── __init__.py
├── client.py # REST API 测试客户端
├── auto_repair/ # 端到端自动化测试
│ ├── client.py # HTTP 测试客户端
│ ├── runner.py # 场景运行器
│ ├── inspector.py # 会话检查器
│ └── scenarios.toml # 测试场景定义
├── tools/
│ └── probe_generated_image_quality.py # 图片质量探针
├── test_main.py # 应用入口测试
├── test_dialog_state.py # 对话状态测试
├── test_canvas_service.py # Canvas 服务测试
├── test_outfit_workflow_service.py # 穿搭工作流测试
└── ... (90+ test files)
Sources: tests/client.py, tests/auto_repair/scenarios.toml
测试分层架构
项目采用四层测试策略,从底层到高层依次为:单元测试、集成测试、API 测试和端到端测试。每一层都有明确的测试目标和覆盖范围。
graph TD
A[端到端测试<br/>auto_repair/scenarios.toml] --> B[API 测试<br/>FastAPI TestClient]
B --> C[集成测试<br/>monkeypatch + Fake 依赖]
C --> D[单元测试<br/>纯函数测试]
A -->|"覆盖完整业务流程"| E[真实业务链路]
B -->|"覆盖 HTTP 接口"| F[路由与依赖注入]
C -->|"覆盖模块交互"| G[服务与工具]
D -->|"覆盖核心逻辑"| H[纯函数与数据结构]
单元测试聚焦纯函数逻辑,不依赖外部服务或复杂状态。典型代表包括定价格式化、对话状态管理、数据序列化等。这类测试执行速度快,断言精确,是回归测试的基础。
Sources: tests/test_pricing.py, tests/test_dialog_state.py
集成测试通过 monkeypatch 和 Fake 对象模拟外部依赖,验证模块间的交互逻辑。这是项目中最常见的测试类型,覆盖了服务层、工具层和消息处理层的核心逻辑。
Sources: tests/test_canvas_service.py, tests/test_outfit_workflow_service.py
API 测试使用 FastAPI 的 TestClient 验证 HTTP 接口的行为,包括请求处理、响应格式、权限控制等。这类测试确保 API 层的契约正确性。
Sources: tests/test_miniapp_outfit_routes.py
端到端测试通过 auto_repair 框架模拟真实用户交互,验证完整业务流程。测试场景定义在 TOML 文件中,包含多轮对话、期望工具调用和关键词断言。
Sources: tests/auto_repair/scenarios.toml, tests/auto_repair/runner.py
单元测试模式
单元测试遵循"Arrange-Act-Assert"模式,专注于验证纯函数的输入输出关系。项目中典型的单元测试包括:
数据格式化测试验证价格、文本等数据的格式化逻辑:
def test_format_cny_price_uses_yuan_symbol() -> None:
assert format_cny_price(None) == ""
assert format_cny_price(399) == "¥399"
assert format_cny_price("399.00") == "¥399"
Sources: tests/test_pricing.py
状态管理测试验证对话状态的初始化、更新和重置逻辑:
def test_activate_skill_initializes_and_loads_state() -> None:
session_state: dict = {}
state = activate_skill(session_state, "style-advisor-skill", sop_stage="collecting_idea")
assert session_state["dialog_state"] is state
assert state["active_skill"] == "style-advisor-skill"
assert state["loaded_skills"] == ["style-advisor-skill"]
assert state["sop_stage"] == "collecting_idea"
Sources: tests/test_dialog_state.py
路径解析测试验证文件路径处理逻辑:
def test_resolve_private_key_path_normalizes_windows_relative_path() -> None:
resolved = _resolve_private_key_path(r"data\ed25519-private.pem")
assert resolved == Path(__file__).resolve().parents[1] / "data" / "ed25519-private.pem"
Sources: tests/test_weather_client.py
集成测试模式
集成测试是项目中最核心的测试类型,通过 monkeypatch 机制注入 Fake 依赖,验证模块间的交互逻辑。这种模式既保持了测试的隔离性,又能验证真实的调用链路。
Fake 对象模式
项目广泛使用 Fake 类模拟外部依赖,典型模式包括:
FakeRedis 模拟 Redis 缓存行为:
class _FakeRedis:
def __init__(self) -> None:
self.values: dict[str, str] = {}
self.setex_calls: list[tuple[str, int, str]] = []
async def get(self, key: str) -> str | None:
return self.values.get(key)
async def setex(self, key: str, ttl: int, value: str) -> bool:
self.values[key] = value
self.setex_calls.append((key, ttl, value))
return True
Sources: tests/test_wechat_api.py, tests/test_live_runs.py
FakeSession 模拟数据库会话:
class FakeSession:
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
return False
Sources: tests/test_canvas_service.py
FakeModel 模拟 LLM 调用:
class FakeModel:
def __init__(self, response_text: str) -> None:
self.response_text = response_text
self.messages = None
async def aresponse(self, messages):
self.messages = messages
return SimpleNamespace(content=self.response_text)
Sources: tests/test_victor_prompt_builder.py
monkeypatch 注入模式
通过 monkeypatch.setattr 注入 Fake 依赖,实现测试隔离:
@pytest.mark.asyncio
async def test_handle_canvas_turn_returns_items_event(monkeypatch: pytest.MonkeyPatch) -> None:
async def fake_load_canvas_runtime_context(**kwargs):
return runtime_context
async def fake_search_items_by_text(**kwargs):
return [{"id": 201}, {"id": 202}]
monkeypatch.setattr(canvas_service, "load_canvas_runtime_context", fake_load_canvas_runtime_context)
monkeypatch.setattr(canvas_service, "search_items_by_text", fake_search_items_by_text)
Sources: tests/test_canvas_service.py
状态清理模式
使用 @pytest.fixture(autouse=True) 自动清理测试状态:
@pytest.fixture(autouse=True)
async def reset_dispatcher_state():
dispatcher._text_tasks.clear()
dispatcher._text_task_contents.clear()
dispatcher._image_processing.clear()
dispatcher._queue_draining.clear()
dispatcher._bg_tasks.clear()
yield
tasks = list(dispatcher._text_tasks.values()) + list(dispatcher._bg_tasks)
for task in tasks:
if task.done():
continue
task.cancel()
for task in tasks:
if task.done():
continue
with contextlib.suppress(asyncio.CancelledError):
await task
Sources: tests/test_session_dispatcher.py
API 测试模式
API 测试使用 FastAPI 的 TestClient 验证 HTTP 接口,覆盖路由处理、依赖注入、响应格式等:
def _build_test_app() -> FastAPI:
app = FastAPI()
app.include_router(miniapp_outfits.router)
app.dependency_overrides[require_login] = lambda: CurrentUser(
user_id=7, token="token", openid="openid"
)
return app
def test_get_or_create_outfit_poster_returns_cached_image(monkeypatch) -> None:
# ... setup fake dependencies ...
response = TestClient(_build_test_app()).post("/api/wechat/outfits/101/poster")
assert response.status_code == 200
assert response.json()["code"] == 200
assert response.json()["data"]["cached"] is True
Sources: tests/test_miniapp_outfit_routes.py
异步测试模式
项目中大量测试涉及异步操作,主要使用两种标记:
@pytest.mark.asyncio 是最常用的异步测试标记,配合 asyncio_mode = "auto" 配置使用:
@pytest.mark.asyncio
async def test_outfit_workflow_service_persists_completed_state(
monkeypatch: pytest.MonkeyPatch,
) -> None:
service = OutfitWorkflowService()
monkeypatch.setattr(service, "get_workflow", lambda: FakeWorkflow())
# ... test logic ...
Sources: tests/test_outfit_workflow_service.py
@pytest.mark.anyio 用于部分需要兼容 AnyIO 的测试:
@pytest.mark.anyio
async def test_auto_test_service_translates_and_executes() -> None:
service = AutoTestService(executor=FakeExecutor(), translator=FakeTranslator())
result = await service.execute_case(AutoTestCaseInput(messages=["你好"]))
assert result.passed is True
Sources: tests/test_auto_test_core.py
端到端自动化测试框架
项目构建了一套完整的端到端自动化测试框架,位于 app/auto_test/ 目录,包含核心引擎、规则系统和执行器。
核心架构
graph LR
A[场景定义<br/>scenarios.toml] --> B[规则引擎<br/>AutoTestEngine]
B --> C[执行器<br/>OutfitAgentAutoTestExecutor]
C --> D[结果评估<br/>AutoTestExecutionResult]
D --> E[报告生成<br/>JSON/Console]
F[自然语言规则] --> G[规则翻译器<br/>OutfitAgentRuleTranslator]
G --> B
规则系统
规则系统采用统一的 target + selector + assertions 模型:
- - target:断言作用对象(execution/message/tool_call)
- - selector:从执行结果中选择节点
- - assertions:对选中节点的断言(exists/equals/contains/sequence_contains 等)
rule = AutoTestRule(
name="profile_tool_called",
target="tool_call",
selector={"name": ["update_user_profile"]},
assertions=[AutoTestAssertion(kind="exists")],
)
Sources: app/auto_test/core/models.py, app/auto_test/core/engine.py
场景定义
测试场景定义在 TOML 文件中,包含多轮对话、期望工具调用和关键词断言:
[[scenarios]]
id = "A-1"
name = "约会穿搭推荐"
category = "outfit_recommendation"
priority = "P0"
messages = ["明晚去上海外滩米其林餐厅约会,帮我搭一套,法式优雅风,预算1000元"]
expected_tools = ["query_weather_now", "search_items"]
expected_keywords = ["搭配", "推荐", "外套", "裙", "上衣", "法式", "优雅", "套", "单品"]
Sources: tests/auto_repair/scenarios.toml
执行流程
端到端测试通过真实业务链路执行,覆盖完整的上下文注入、工具调用和状态管理:
async def run_single_test(case: TestCase) -> TestResult:
session_id = str(uuid.uuid4())
service = get_outfit_agent_auto_test_service()
execution = await service.execute_case(
AutoTestCaseInput(
name=case.id,
messages=case.messages,
rules=build_legacy_rules(
expected_tools=case.expected_tools,
expected_keywords=case.expected_keywords,
),
metadata={"session_id": session_id},
)
)
# ... evaluate results ...
Sources: tests/auto_repair/runner.py, app/auto_test/outfit_agent/executor.py
测试覆盖模块
项目的测试覆盖了主要业务模块,按功能域划分:
| 测试域 | 测试文件 | 覆盖范围 |
|---|---|---|
| Admin 管理 | test_admin_*.py (15个) | 权限控制、配置管理、监控接口 |
| Canvas 画布 | test_canvas_*.py (6个) | 画布服务、会话管理、路由处理 |
| 消息处理 | test_messaging_.py, test_miniapp_.py | 消息入站、会话分发、WebSocket |
| 工具系统 | test_tool_*.py (5个) | 工具注册、工厂、门控机制 |
| 穿搭工作流 | test_outfit_*.py (7个) | 工作流服务、工具、生成流程 |
| 微信集成 | test_wechat_*.py (7个) | API 接口、意图仲裁、回调处理 |
| Agent 核心 | test_agent*.py, test_agentos.py | Agent 配置、运行时上下文 |
| 记忆与洞察 | test_insights.py, test_memory_diag.py | 洞察分析、记忆管理 |
| 可观测性 | test_observability.py, test_llm_logging.py | 监控、日志、链路追踪 |
Sources: tests/
Live 测试与探针工具
项目包含可选的 Live 测试,用于验证真实外部服务的一致性:
def test_text_embedding_batch_matches_single_requests_exactly() -> None:
_require_live_embedding_tests()
batch_vectors = _call_text_embedding(_TEXT_ITEMS)
single_vectors = [_call_text_embedding([text])[0] for text in _TEXT_ITEMS]
comparisons = _compare_vectors(batch_vectors, single_vectors)
assert all(result.exact_match for result in comparisons)
运行方式:RUN_LIVE_EMBEDDING_TESTS=1 uv run pytest tests/test_embedding_batch_consistency_live.py -q
Sources: tests/test_embedding_batch_consistency_live.py
此外,tests/tools/probe_generated_image_quality.py 提供图片质量探针工具,用于验证图片生成、裁剪和水印处理流程。
Sources: tests/tools/probe_generated_image_quality.py
测试最佳实践
基于项目代码库的分析,总结以下测试最佳实践:
1. 依赖隔离优先:所有外部依赖(Redis、数据库、LLM、第三方 API)都通过 monkeypatch 注入 Fake 对象,确保测试隔离性和可重复性。
2. 状态清理自动化:使用 @pytest.fixture(autouse=True) 自动清理模块级状态,避免测试间干扰。
3. 断言精确化:优先使用结构化断言(工具调用序列、状态推进、字段值),而非模糊的文本匹配。
4. 测试数据内联:测试数据直接定义在测试函数内,保持测试的自包含性。
5. 异步测试规范:统一使用 @pytest.mark.asyncio 标记异步测试,配合 asyncio_mode = "auto" 配置。
6. 分层覆盖策略:单元测试覆盖核心逻辑,集成测试覆盖模块交互,API 测试覆盖接口契约,端到端测试覆盖业务流程。
运行测试指南
运行全部测试:
uv run pytest tests/ -v
运行特定模块测试:
uv run pytest tests/test_dialog_state.py -v
运行异步测试:
uv run pytest tests/test_outfit_workflow_service.py -v --asyncio-mode=auto
运行端到端场景测试:
uv run tests/auto_repair/runner.py
运行 Live 测试(需要真实 API Key):
RUN_LIVE_EMBEDDING_TESTS=1 uv run pytest tests/test_embedding_batch_consistency_live.py -q
使用交互式测试客户端:
uv run tests/client.py i # 普通交互
uv run tests/client.py i --sse # SSE 流式交互
Sources: tests/client.py, pyproject.toml
下一步阅读
完成本篇后,建议继续阅读以下文档以深入了解相关主题:
- - 自动化测试与修复:了解更高级的自动化测试与自动修复机制
- - 工具注册与工厂:理解工具系统的架构,有助于编写工具测试
- - Dialog State 设计:深入理解对话状态管理,有助于编写状态相关测试
- - 链路追踪与 Trace:了解链路追踪系统,与端到端测试框架紧密相关
<a id="28-zi-dong-hua-ce-shi-yu-xiu-fu"></a>
---
自动化测试与修复
- - Section: 开发与测试
- - Source File:
28-zi-dong-hua-ce-shi-yu-xiu-fu.md
本文档介绍 Outfit Agent 的自动化测试与修复系统,涵盖核心架构、规则引擎、执行流程以及快速上手指南。该系统旨在将人工测试转化为可重复执行的自动化回归测试,并通过统一的规则体系验证 Agent 行为的正确性。
系统架构总览
自动化测试系统采用分层架构设计,从上到下分为四层:规则编译层、执行编排层、断言评估层 和 结果展示层。整个流程的核心链路是:自然语言规则 → 结构化规则 → Agent 执行 → 断言评估 → 测试报告。
graph TB
subgraph "规则编译层"
NL["自然语言规则"]
Translator["OutfitAgentRuleTranslator"]
Rules["结构化 AutoTestRule"]
end
subgraph "执行编排层"
Service["AutoTestService"]
Executor["OutfitAgentAutoTestExecutor"]
Agent["Outfit Agent 真实链路"]
end
subgraph "断言评估层"
Engine["AutoTestEngine"]
Artifact["AutoTestExecutionArtifact"]
Results["AutoTestRuleResult[]"]
end
subgraph "结果展示层"
AdminAPI["Admin API"]
Dashboard["Dashboard UI"]
Report["测试报告"]
end
NL --> Translator
Translator --> Rules
Rules --> Service
Service --> Executor
Executor --> Agent
Agent --> Artifact
Artifact --> Engine
Engine --> Results
Results --> AdminAPI
AdminAPI --> Dashboard
Dashboard --> Report
Sources: app/auto_test/core/models.py, app/auto_test/core/engine.py, app/auto_test/core/service.py
核心组件详解
规则模型(AutoTestRule)
规则是自动化测试的核心抽象,采用 target + selector + assertions 的统一模型。每个规则包含以下关键字段:
| 字段 | 类型 | 说明 | |
|---|---|---|---|
name | str | 规则名称,建议使用 snake_case | |
target | Literal | 断言作用对象:execution、message、tool_call | |
selector | dict | 从执行结果中筛选节点的条件 | |
assertions | list[AutoTestAssertion] | 断言列表,至少包含一个 | |
severity | Literal | 失败严重级别:error、warning、info | |
description | `str | None` | 规则描述,用于报告展示 |
Sources: app/auto_test/core/models.py
断言类型(AutoTestAssertionKind)
系统支持 8 种断言类型,覆盖常见的测试场景:
| 断言类型 | 说明 | 必需字段 |
|---|---|---|
exists | 选中节点存在 | 无 |
equals | 字段值等于指定值 | field, value |
contains | 字段值包含指定字符串 | field, value |
not_contains | 字段值不包含指定字符串 | field, value |
contains_any | 字段值包含任意一个指定值 | field, values |
contains_all | 字段值包含所有指定值 | field, values |
min_count | 选中节点数量不少于指定值 | min_count |
sequence_contains | 工具调用序列包含指定子序列 | values |
Sources: app/auto_test/core/models.py
执行引擎(AutoTestEngine)
执行引擎负责评估规则,其核心逻辑分为三个步骤:
- 1. 选择节点(
_select_items):根据target和selector从执行产物中筛选待检查的节点 - 2. 匹配选择器(
_matches_selector):支持精确匹配和列表匹配 - 3. 评估断言(
_evaluate_assertion):对选中节点执行具体的断言逻辑
flowchart LR
A["输入: Artifact + Rules"] --> B["遍历每条规则"]
B --> C["选择节点: _select_items"]
C --> D["匹配选择器: _matches_selector"]
D --> E["评估断言: _evaluate_assertion"]
E --> F["输出: AutoTestRuleResult"]
Sources: app/auto_test/core/engine.py
规则翻译器(OutfitAgentRuleTranslator)
规则翻译器将自然语言描述转换为结构化规则,使用 LLM 进行语义理解。翻译过程遵循以下流程:
sequenceDiagram
participant User as 用户
participant Translator as OutfitAgentRuleTranslator
participant LLM as LLM (Flash)
participant Parser as JSON 解析器
User->>Translator: "必须调用 search_items"
Translator->>LLM: 构造提示词 + 自然语言规则
LLM->>Translator: 返回 JSON 格式规则
Translator->>Parser: 提取 JSON 对象
Parser->>Translator: 返回结构化规则
Translator->>User: AutoTestRuleTranslationResult
翻译提示词定义了详细的转换规则,例如:
- - "必须调用某工具" →
target=tool_call, selector.name=工具名, assertions=[{"kind":"exists"}] - - "回复必须包含某些要点" →
target=execution, field=reply_text, kind=contains_all
Sources: app/auto_test/outfit_agent/translator.py, data/prompts/auto_tests/rule_translation.zh.txt
执行流程
完整执行链路
自动化测试的完整执行链路如下:
flowchart TD
A["构造 AutoTestCaseInput"] --> B["AutoTestService.execute_case"]
B --> C{是否有自然语言规则?}
C -->|是| D["调用 Translator 翻译规则"]
C -->|否| E["使用预定义规则"]
D --> F["合并所有规则"]
E --> F
F --> G["调用 Executor 执行"]
G --> H["运行真实 Agent 链路"]
H --> I["收集执行产物 AutoTestExecutionArtifact"]
I --> J["Engine.evaluate 评估规则"]
J --> K["生成 AutoTestExecutionResult"]
Sources: app/auto_test/core/service.py
OutfitAgentAutoTestExecutor
执行器负责运行真实的 Agent 链路,其核心流程包括:
- 1. 构造执行上下文:解析元数据中的
session_id、channel、user_id - 2. 逐条消息执行:对每条用户消息调用
run_inbound_agent_turn - 3. 收集执行产物:从数据库加载 session runs,提取 messages 和 tool_calls
- 4. 错误检测:识别
_NO_CTX_RESULT和工具错误
关键设计点:
- - 使用真实的业务链路
/api/chat而非绕过业务层 - - 自动构建
ToolContext确保覆盖完整业务逻辑 - - 支持多渠道(
wechat_miniapp、wechat_kefu)
Sources: app/auto_test/outfit_agent/executor.py
Session Inspector
Session Inspector 负责从数据库加载和解析执行数据,核心功能包括:
| 方法 | 功能 |
|---|---|
load_session_runs | 从 agno.agno_sessions 加载 runs 数据 |
build_run_messages | 解析 run 中的 messages 为 AutoTestMessage 列表 |
build_run_tool_calls | 提取工具调用为 AutoTestToolCall 列表 |
extract_run_reply | 提取 assistant 的最终回复 |
数据来源为 PostgreSQL 的 agno schema,关键表结构:
- -
agno.agno_sessions:存储 session 级数据,包含runsJSONB 数组 - -
runs数组中的每个元素包含messages、tools、run_id等字段
Sources: app/auto_test/outfit_agent/session_inspector.py
Admin API 接口
系统提供两个 Admin API 端点,均需要管理员权限:
规则翻译接口
POST /api/admin/auto-tests/rules/translate
请求体:
{
"natural_language": "必须调用 search_items",
"lang": "zh"
}
响应体:
{
"natural_language": "必须调用 search_items",
"rules": [
{
"name": "must_call_search",
"target": "tool_call",
"selector": {"name": "search_items"},
"assertions": [{"kind": "exists"}],
"severity": "error"
}
],
"raw_payload": {...}
}
测试执行接口
POST /api/admin/auto-tests/executions
请求体:
{
"name": "case-1",
"messages": ["你好", "继续"],
"natural_language_rule": "回复里要包含推荐",
"metadata": {"user_id": 7, "channel": "wechat_miniapp"}
}
响应体包含完整的执行结果,包括 passed、rule_results、artifact 等字段。
Sources: app/routers/admin/auto_tests.py
测试场景配置
场景文件结构
测试场景定义在 tests/auto_repair/scenarios.toml 中,采用 TOML 格式,每个场景包含以下字段:
| 字段 | 类型 | 说明 |
|---|---|---|
id | str | 场景唯一标识,如 A-1、I-2 |
name | str | 场景名称 |
category | str | 场景分类 |
priority | str | 优先级:P0、P1、P2 |
messages | list[str] | 用户消息列表,支持多轮对话 |
expected_tools | list[str] | 期望调用的工具列表 |
expected_keywords | list[str] | 期望回复包含的关键词 |
notes | str | 备注说明 |
场景分类
测试场景按业务功能分为以下类别:
| 类别 | 说明 | 示例场景 |
|---|---|---|
| I - 用户画像 | 更新/查询身材偏好 | I-1: 更新用户画像 |
| A - 有场景穿搭推荐 | 职场/约会/社交场景 | A-1: 约会穿搭推荐 |
| B - 模糊意图 | 空意图或模糊需求 | B-1: 模糊意图-日常上班 |
| H - 衣橱管理 | 查询/修改/浏览单品 | H-1: 浏览衣橱 |
| G - 衣橱录入 | 上传衣物照片 | G-1: 记录穿着日志 |
| K - 穿搭历史 | 查看保存的搭配方案 | K-1: 查看穿搭历史 |
| J - 购物/比价 | 搜索商品、查询购买链接 | J-1: 搜索莫兰迪色毛衣 |
| F - 找同款 | 图片或文字找商品 | F-1: 文字找同款 |
| C - Complete the Look | 基于衣橱单品搭配 | C-1: 基于衣橱单品搭配 |
| D - Get the Look | OOTD 参考图找相似 | D-1: OOTD参考图找相似穿搭 |
Sources: tests/auto_repair/scenarios.toml
优先级策略
| 优先级 | 说明 | 使用场景 |
|---|---|---|
P0 | 核心功能,必须通过 | CI/CD 流水线、发布前检查 |
P1 | 重要功能,建议通过 | 日常回归测试 |
P2 | 边缘场景,可选通过 | 完整性测试 |
测试执行方式
方式一:使用 Legacy Runner
Legacy Runner 是早期实现的测试执行器,支持从 scenarios.toml 加载场景并执行:
# 运行所有场景
uv run python -m tests.auto_repair.runner
# 只运行 P0 优先级场景
uv run python -m tests.auto_repair.runner --priority P0
# 运行指定场景
uv run python -m tests.auto_repair.runner --cases A-1,A-2,I-1
执行流程:
- 1. 从
scenarios.toml加载场景定义 - 2. 调用
build_legacy_rules将expected_tools和expected_keywords转换为规则 - 3. 使用
AutoTestService执行测试 - 4. 生成结果报告并保存到
tests/auto_repair/results/目录
Sources: tests/auto_repair/runner.py
方式二:使用 Admin API
通过 Admin API 可以在 Dashboard 中直接执行测试:
- 1. 规则翻译:将自然语言规则转换为结构化规则
- 2. 测试执行:构造测试用例并执行
- 3. 结果查看:在 Dashboard 中查看详细的执行结果
方式三:编写单元测试
可以编写单元测试来验证规则引擎和翻译器的正确性:
from app.auto_test.core.engine import AutoTestEngine
from app.auto_test.core.models import (
AutoTestAssertion, AutoTestCaseInput,
AutoTestExecutionArtifact, AutoTestRule, AutoTestToolCall
)
# 构造执行产物
artifact = AutoTestExecutionArtifact(
session_id="test-session",
replies=["好的,已经帮你记录。"],
final_reply="好的,已经帮你记录。",
tool_calls=[
AutoTestToolCall(name="update_user_profile", status="OK"),
],
)
# 定义规则
rules = [
AutoTestRule(
name="profile_tool_called",
target="tool_call",
selector={"name": ["update_user_profile"]},
assertions=[AutoTestAssertion(kind="exists")],
),
]
# 评估规则
results = AutoTestEngine().evaluate(artifact, rules)
assert results[0].passed is True
Sources: tests/test_auto_test_core.py
Inspector 工具
Inspector 工具用于直接查询数据库,获取 Agent 执行的内部细节:
核心功能
| 方法 | 功能 | 使用场景 |
|---|---|---|
get_runs | 获取指定 session 的 run 记录 | 查看执行历史 |
extract_tool_calls | 提取工具调用列表 | 分析工具使用情况 |
extract_errors | 提取错误信息 | 定位执行失败原因 |
check_no_ctx | 检测 ToolContext 注入问题 | 诊断上下文缺失 |
analyze_session | 综合分析 session 执行情况 | 全面诊断 |
使用示例
from tests.auto_repair.inspector import AgnoInspector
inspector = AgnoInspector()
# 分析指定 session
analysis = await inspector.analyze_session("session-id-here")
print(f"Run 数量: {analysis['run_count']}")
print(f"调用工具: {analysis['tools_called']}")
print(f"错误信息: {analysis['errors']}")
print(f"是否有 _NO_CTX_RESULT: {analysis['has_no_ctx']}")
Sources: tests/auto_repair/inspector.py
自动修复机制
自动修复系统基于测试失败的诊断结果,自动定位问题并生成修复建议。整体流程分为六个阶段:
flowchart TD
A["Phase 1: 场景生成"] --> B["Phase 2: 测试执行"]
B --> C["Phase 3: 细节查询"]
C --> D["Phase 4: 根因分析"]
D --> E["Phase 5: 自动修复"]
E --> F["Phase 6: 回归验证"]
A1["ScenarioAgent"] -.-> A
B1["TriggerAgent"] -.-> B
C1["InspectorAgent"] -.-> C
D1["DiagnosticAgent"] -.-> D
E1["RepairAgent"] -.-> E
F1["VerifyAgent"] -.-> F
问题类型与修复策略
| 问题类型 | 诊断信号 | 修复位置 |
|---|---|---|
| 提示词不明确 | messages 无 tool_calls | data/prompts/agents/*.txt |
| 工具参数错误 | tool_calls 中 error 字段 | app/tools/*.py 参数处理 |
| service 层 Bug | tool result 含 "error" | outfit_biz/src/outfit_biz/services/*.py |
| ToolContext 未注入 | _NO_CTX_RESULT 出现 | app/routers/chat.py ToolContext 构建 |
| 记忆检索失败 | memory_context 为空但应有 | app/memory/ops.py |
修复原则
- 1. 最小化改动:只修复根因,避免引入新问题
- 2. 遵循 AGENTS.md 规约:async IO、禁止硬编码文本、错误信息用英文
- 3. 补充单元测试:修复后必须新增测试用例覆盖正常路径和边界情况
- 4. 回归验证:修复后重跑失败场景,确保问题已解决
Sources: docs/auto_repair.md
最佳实践
规则编写建议
- 1. 优先使用结构化断言:避免依赖 LLM 进行语义判断
- 2. 明确选择器条件:使用精确的
selector筛选目标节点 - 3. 合理设置严重级别:核心功能用
error,边缘场景用warning - 4. 添加规则描述:便于理解规则意图和调试失败原因
测试场景设计
- 1. 覆盖核心路径:确保 P0 场景覆盖所有核心功能
- 2. 考虑边界情况:无头像、无衣橱、quota 耗尽等场景
- 3. 支持多轮对话:使用
messages列表模拟真实对话流程 - 4. 合理设置期望:
expected_tools和expected_keywords不宜过于严格
执行策略
- 1. CI/CD 集成:将 P0 场景集成到发布流水线
- 2. 定期回归:每周运行完整测试套件
- 3. 失败分析:及时分析失败用例,区分环境问题和代码问题
- 4. 持续优化:根据测试结果调整规则和场景
常见问题
Q1: 测试执行时出现 _NO_CTX_RESULT 错误
原因:ToolContext 未正确注入,通常是测试方式不正确导致。
解决方案:
- - 使用
/api/chat接口进行测试,确保走真实业务链路 - - 检查
user_id和session_id是否正确配置 - - 确认测试环境的数据库连接正常
Q2: 规则翻译结果不准确
原因:自然语言描述过于模糊或复杂。
解决方案:
- - 使用更明确的描述,如 "必须调用 search_items 工具"
- - 避免使用模糊的词汇,如 "可能"、"也许"
- - 对于复杂规则,建议直接编写结构化规则
Q3: 测试结果不稳定
原因:Agent 回复的非确定性导致。
解决方案:
- - 避免使用精确匹配断言,优先使用
contains、contains_any等模糊匹配 - - 使用
expected_keywords而非精确回复文本 - - 对于工具调用顺序,使用
sequence_contains而非精确匹配
相关文档
- - 单元测试与集成测试:了解项目的测试框架和最佳实践
- - 链路追踪与 Trace:了解如何查看 Agent 执行的详细链路
- - Dashboard 管理面板:了解如何在 Dashboard 中管理自动化测试
- - 新增业务工具:了解如何为新工具编写测试场景
<a id="29-bu-shu-yu-fa-bu"></a>
---
部署与发布
- - Section: 开发与测试
- - Source File:
29-bu-shu-yu-fa-bu.md
本文档详述 outfit-agent 服务从代码到生产的完整部署流程,涵盖环境依赖、配置管理、进程守护、健康检查、运维脚本以及生产环境注意事项。outfit-agent 基于 FastAPI + uvicorn 构建,使用 PM2 进行进程管理,依赖 PostgreSQL、Redis 和外部 LLM 服务。
部署架构总览
outfit-agent 的生产部署涉及多个组件协同工作,下图展示了完整的部署拓扑:
graph TB
subgraph "外部依赖"
LLM["LLM 服务<br/>(OpenAI / DashScope)"]
WXP["企业微信平台"]
ALY["阿里云内容安全"]
WXW["和风天气"]
M0["Mem0 云服务"]
end
subgraph "服务器 (单机部署)"
PM2["PM2 进程管理器"]
UV["uvicorn ASGI Server"]
APP["outfit-agent<br/>(FastAPI App)"]
DB["PostgreSQL"]
RD["Redis"]
DASH["Dashboard SPA<br/>(Vite + React)"]
end
subgraph "关联项目"
BIZ["outfit_biz<br/>(业务基础库)"]
end
PM2 -->|守护进程| UV
UV -->|ASGI 协议| APP
APP -->|会话持久化 / 追踪| DB
APP -->|缓存 / 队列| RD
APP --> LLM
APP --> WXP
APP --> ALY
APP --> WXW
APP --> M0
APP -.->|本地依赖| BIZ
APP -->|静态文件| DASH
Sources: ecosystem.config.js, app/main.py
环境要求
部署 outfit-agent 需要满足以下基础环境要求:
| 组件 | 版本要求 | 说明 |
|---|---|---|
| Python | >= 3.11 | 推荐 3.12,项目使用 requires-python = ">=3.11" |
| uv | 最新版 | 用于依赖安装和运行,替代传统 pip |
| PostgreSQL | >= 14 | 存储会话、追踪、灰度配置等数据 |
| Redis | >= 6 | 用于缓存、队列、Token 管理 |
| Node.js | >= 18 | Dashboard 前端构建(Vite 8) |
| PM2 | >= 5 | 进程守护与管理 |
| outfit_biz | 本地路径依赖 | 必须与 outfit_agent 同级目录部署 |
项目使用 pyproject.toml 管理依赖,构建后端为 hatchling。关键依赖包括 agno(AI Agent 框架)、FastAPI、OpenTelemetry 系列、mem0ai 等。详见 pyproject.toml 中的 dependencies 列表。
Sources: pyproject.toml, requirements.txt
部署步骤
1. 代码拉取
在服务器上选择一个目录(如 /root/outfit),同时拉取两个项目,确保目录结构如下:
/root/outfit/
├── outfit_agent/ # 本项目
└── outfit_biz/ # 业务基础库(本地路径依赖)
两个项目必须处于同一父目录下,因为 pyproject.toml 中通过 outfit-biz = { path = "../outfit_biz", editable = true } 引用业务基础库,运行时代码中也通过相对路径查找配置文件。
Sources: pyproject.toml, app/config.py
2. 安装依赖
进入 outfit_agent 目录,使用 uv 安装依赖(含开发依赖):
cd /root/outfit/outfit_agent
uv sync --dev
uv sync 会读取 pyproject.toml 和 uv.lock,自动创建虚拟环境并安装所有依赖。--dev 参数包含 pytest、ruff 等开发工具。如果仅部署生产环境,可省略 --dev。
Sources: pyproject.toml, docs/deployment.md
3. 环境变量配置
将 .env.example 复制为 .env 并填入实际配置:
cp .env.example .env
环境变量分为以下几大类:
| 类别 | 关键变量 | 说明 |
|---|---|---|
| LLM | OPENAI_API_KEY, OPENAI_BASE_URL | LLM 服务密钥与网关地址,推荐统一走 OpenAI 兼容网关 |
| Agent | AGENT_NAME | Agent 实例名称 |
| 服务 | HOST, PORT, LOG_LEVEL | uvicorn 绑定地址与日志级别 |
| 企业微信 | WECHAT_TOKEN, WECHAT_SECRET, WECHAT_CORP_ID 等 | 企业微信客服与小程序集成凭证 |
| 阿里云 | ALIYUN_AK_ID, ALIYUN_AK_SECRET | 内容安全审核服务凭证 |
| 天气 | QWEATHER_API_HOST, QWEATHER_KEY_ID 等 | 和风天气 API 配置 |
| 记忆 | MEM0_ENABLED, MEM0_COLLECTION_NAME | Mem0 长期记忆组件开关 |
| 监控 | HYPERDX_API_KEY, OTEL_SERVICE_NAME | HyperDX 可观测性配置 |
| Admin | ADMIN_SECRET_KEY | Admin Dashboard 访问令牌,生产环境必须设置 |
.env 文件已被 .gitignore 排除,切勿提交包含密钥的文件到版本控制。数据库 URL(DATABASE_URL)和 Redis URL(REDIS_URL)在 outfit_biz 侧统一管理。
Sources: .env.example
4. 数据库初始化
项目使用 PostgreSQL 存储会话、追踪和业务数据。启动时应用会自动执行轻量级的 Schema 确保操作(非正式迁移),包括创建缺失的列、索引和数据迁移。核心确保逻辑位于 _mem0_lifespan 中,涵盖以下表结构:
- -
users:添加nick_name列 - -
outfits:添加failure_reason列 - -
items:重命名处理状态列、添加init_status/init_error列 - -
gray_config_rules:灰度配置表 - -
user_insights:用户洞察表 - -
message_access_control:消息准入控制表 - -
message_interception:消息拦截表
如果没有正式的数据库迁移工具,这些 ALTER TABLE ... ADD COLUMN IF NOT NULL 语句确保代码运行时不会因缺少列而失败。
Sources: app/main.py
5. 和风天气密钥生成
参考和风天气官方文档生成 Ed25519 私钥,放置于 data/ed25519-private.pem。注意需要在控制台充值以激活 API 服务。
Sources: docs/deployment.md, .env.example
6. Dashboard 前端构建
Dashboard 管理面板基于 React + Vite 构建,需要单独编译:
cd dashboard
npm install
npm run build
构建产物输出到 dashboard/dist/ 目录,应用启动时会自动检测该目录并以静态文件方式挂载到 /dashboard/ 路径下。如果目录不存在,Dashboard 功能将不可用但不影响核心服务。
Sources: app/main.py, dashboard/package.json
进程管理
PM2 配置
项目使用 PM2 进行进程守护,配置文件为 ecosystem.config.js:
module.exports = {
apps: [
{
name: "outfit-agent",
script: "uv",
args: "run uvicorn app.main:app --host 127.0.0.1 --port 8897",
cwd: "/root/outfit/outfit_agent",
interpreter: "none",
env_file: ".env",
autorestart: true,
watch: false,
max_memory_restart: "1G",
log_date_format: "YYYY-MM-DD HH:mm:ss",
},
],
};
关键配置说明:
| 参数 | 值 | 说明 |
|---|---|---|
name | outfit-agent | PM2 进程名称,用于 pm2 status / pm2 logs 识别 |
script | uv | 使用 uv 作为进程启动器,自动处理虚拟环境 |
args | run uvicorn app.main:app ... | uvicorn 启动参数,绑定 127.0.0.1:8897 |
cwd | /root/outfit/outfit_agent | 工作目录,必须为项目根目录 |
interpreter | none | 不使用 Node.js 解释器,直接执行 uv 命令 |
env_file | .env | 自动加载环境变量文件 |
autorestart | true | 进程崩溃后自动重启 |
max_memory_restart | 1G | 内存超过 1GB 自动重启,防止内存泄漏 |
watch | false | 生产环境禁用文件监听,避免意外重启 |
启动服务:
pm2 start ecosystem.config.js
Sources: ecosystem.config.js
直接启动(开发环境)
开发环境下可直接使用 uvicorn 启动,支持热重载:
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
或通过 app/main.py 中的 __main__ 入口:
python -m app.main
Sources: app/main.py, README.md
启动生命周期
应用启动时经历以下初始化序列,各组件按严格顺序就绪:
sequenceDiagram
participant PM2 as PM2
participant UV as uvicorn
participant APP as FastAPI App
participant AGENT as Agent
participant DB as PostgreSQL
participant RD as Redis
participant Q as 队列处理器
PM2->>UV: 启动进程
UV->>APP: ASGI lifespan 启动
APP->>APP: _init_httpx_client()
APP->>AGENT: init_agent() / init_canvas_agent()
APP->>APP: init_hyperdx_monitoring()
APP->>DB: Schema 确保 (ALTER IF NOT EXISTS)
APP->>DB: ensure_gray_config_schema()
APP->>DB: ensure_insight_schema()
APP->>DB: ensure_message_access_control_schema()
APP->>DB: ensure_message_interception_schema()
APP->>APP: ensure_insight_schedule() (Cron)
APP->>RD: init_memory() / _init_litellm_redis_cache()
APP->>DB: recover_interrupted_workflows()
APP->>Q: ImgGenHandler.start()
APP->>Q: ImageCaptionHandler.start()
APP->>Q: ItemProcessingHandler.start()
APP-->>UV: 应用就绪
启动过程中有三个后台队列处理器被启动:
- - ImgGenHandler:图片生成队列消费,最大并发 3,数据库兜底扫描间隔 180 秒
- - ImageCaptionHandler:图片描述异步补全,最大并发 2
- - ItemProcessingHandler:衣橱单品异步处理,最大并发 2
此外,recover_interrupted_workflows() 会在启动时恢复中断的穿搭工作流会话,确保异常重启不丢失任务状态。
Sources: app/main.py, app/config.py
健康检查
服务提供标准 HTTP 健康检查端点:
GET /health
返回 {"status": "ok"},可用于负载均衡器、容器编排平台或外部监控系统探测服务存活状态。
Sources: app/main.py
可观测性
OpenTelemetry + HyperDX
项目集成了 OpenTelemetry 日志导出,默认对接 HyperDX 平台。配置以下环境变量即可启用:
- -
HYPERDX_API_KEY:HyperDX API 密钥 - -
OTEL_SERVICE_NAME:服务名称标识
日志通过 OTLP HTTP 协议导出到 https://in-otel.hyperdx.io(可通过 HYPERDX_API_ENDPOINT 或 OTEL_EXPORTER_OTLP_ENDPOINT 自定义)。此外还支持 Agno 框架级的 DB Span 追踪,追踪数据写入 PostgreSQL 的 agno schema。
Sources: app/observability.py, app/agentos.py
AgentOS 仪表板
AgentOS 提供了内置的管理能力,包括:
- - Tracing:OpenTelemetry 追踪,数据持久化到 PostgreSQL
- - Scheduler:Cron 调度器,通过
/v1/schedules管理定时任务 - - MCP Server:暴露 MCP 服务端点
AgentOS 通过 base_app 模式挂载到现有 FastAPI 应用上,保留所有业务路由,路由冲突时优先保留业务路由(on_route_conflict="preserve_base_app")。
Sources: app/agentos.py
配置体系
项目采用双层配置合并机制:outfit_biz/config.yml 作为基础配置,outfit_agent/config.yml 作为覆盖层。两者通过深度合并(deep-merge)生成最终运行时配置。环境变量通过 .env 文件注入,pydantic-settings 负责类型校验和默认值。
graph LR
BIZ["outfit_biz/config.yml<br/>(基础配置)"]
AGENT["outfit_agent/config.yml<br/>(覆盖配置)"]
ENV[".env<br/>(环境变量)"]
MERGED["合并后配置<br/>(AgentSettings)"]
APP["FastAPI App"]
BIZ -->|deep_merge| MERGED
AGENT -->|deep_merge| MERGED
ENV -->|pydantic-settings| MERGED
MERGED -->|get_settings()| APP
config.yml 中的 agent.team 和 agent.member 节的键值会透传给 Agno 的 Team / Agent 构造函数,仅支持可序列化的标量类型参数,需要运行时对象的参数(如 model、db、tools)在代码中设置。
Sources: app/config.py, config.yml
运维脚本
项目提供以下运维脚本,位于 scripts/ 目录下:
| 脚本 | 用途 | 说明 |
|---|---|---|
ensure_gray_config_schema.py | 确保灰度配置表结构 | 独立运行,不依赖完整应用启动 |
backfill_outfit_messages.py | 回填历史穿搭消息 | 将旧版 outfits 消息转换为结构化卡片格式,支持 --apply 和 --limit |
backfill_generate_outfit_trace_sessions.py | 回填穿搭生成追踪会话 | 补全历史追踪数据 |
issue_miniapp_test_token.py | 签发小程序测试 Token | 支持通过 --user-id 或 --outfit-id 指定目标用户 |
canvas_ws_cli.py | Canvas WebSocket CLI | Canvas 功能调试工具 |
使用示例:
# 确保灰度配置表
uv run python scripts/ensure_gray_config_schema.py
# 回填穿搭消息(试运行)
uv run python scripts/backfill_outfit_messages.py --limit 50
# 回填穿搭消息(实际执行)
uv run python scripts/backfill_outfit_messages.py --limit 50 --apply
# 签发测试 Token
uv run python scripts/issue_miniapp_test_token.py --user-id 123 --ttl 3600
Sources: scripts/ensure_gray_config_schema.py, scripts/backfill_outfit_messages.py, scripts/issue_miniapp_test_token.py
Firecrawl 服务(辅助)
项目使用 Firecrawl 进行网页抓取,作为独立的 Docker Compose 部署。API 地址为 http://47.101.211.109:3002,组件包括:
| 组件 | 端口 | 说明 |
|---|---|---|
| api + nuq-workers | 3002 | Firecrawl 主服务(host 网络) |
| playwright-service | 3000 | 无头浏览器(host 网络) |
| rabbitmq | 5672 | 任务队列 |
| Redis | 6379 | 使用已有服务 |
| PostgreSQL | 5433 | 独立数据库 firecrawl |
# 查看 Firecrawl 服务状态
docker compose -f /opt/firecrawl/docker-compose.yaml ps
# 抓取单页
curl -X POST http://47.101.211.109:3002/v1/scrape \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com","formats":["markdown"]}'
Sources: docs/deployment.md
生产环境注意事项
内存管理:PM2 配置了 max_memory_restart: "1G",当进程内存超过限制时自动重启。Agent 启动过程中加载的全局 httpx 客户端连接池上限为 1000 连接、200 保持连接,超时 120 秒。Sources: ecosystem.config.js, app/main.py
LLM 超时:默认 LLM 调用超时为 600 秒(10 分钟),最大重试 0 次。Agent 层面 Team 配置了 2 次重试和指数退避。可通过 config.yml 中的 llm_chat_timeout_seconds 和 llm_chat_max_retries 调整。Sources: config.yml, config.yml
内容安全:阿里云内容安全审核默认开启,建议仅对小程序渠道开启(aliyun_content_audit_enabled_channels: wechat_miniapp),因客服渠道误报率较高。文本和图片审核的置信度阈值均设置为 95。Sources: config.yml
LiteLLM 缓存:可通过 LITELLM_REDIS_CACHE_ENABLED=true 启用 LLM 响应的 Redis 缓存,相同请求直接命中缓存,降低延迟与 API 成本。默认 TTL 为 3600 秒。Sources: .env.example, app/main.py
安全凭证:ADMIN_SECRET_KEY 必须在生产环境设置为随机长字符串,留空则所有 /api/admin/* 接口返回 403。DEV_TOKEN 和 DEV_USER_ID 仅用于开发环境,生产环境务必留空。Sources: .env.example
相关文档
部署完成后,建议继续阅读以下文档以全面了解系统:
- - 环境变量配置:详细的环境变量说明
- - 配置文件结构:config.yml 合并机制与参数说明
- - 系统架构总览:整体架构设计
- - 系统监控与健康检查:监控体系建设
- - Admin API 接口:管理接口使用
<a id="30-xin-zeng-ye-wu-gong-ju"></a>
---
新增业务工具
- - Section: 扩展与定制
- - Source File:
30-xin-zeng-ye-wu-gong-ju.md
本文档面向希望为 Outfit Agent 系统添加新业务工具的开发者。工具系统采用 agno 框架的 Function 类,通过工厂模式统一封装、注册和管理。新增工具需要遵循严格的注册流程和安全规范,确保 LLM 只能访问业务意图参数,而用户身份信息通过 RunContext 自动注入。
工具系统架构概览
Outfit Agent 的工具系统由三个核心层组成:工具实现层、注册管理层和运行时门控层。工具实现层包含具体的业务逻辑函数;注册管理层通过工厂模式将函数包装为 agno Function 对象并维护全局注册表;运行时门控层则基于技能激活状态和渠道权限控制工具的可用性。
graph TB
A[业务需求] --> B[编写工具函数]
B --> C[添加工具描述]
C --> D[注册到模块]
D --> E[注册到全局表]
E --> F[配置技能/团队]
F --> G[运行时门控]
subgraph "工具实现层"
B
end
subgraph "注册管理层"
C
D
E
end
subgraph "运行时门控层"
F
G
end
Sources: app/tools/factory.py, app/tools/__init__.py
新增工具的标准流程
新增一个业务工具需要完成以下五个步骤:编写工具函数、添加工具描述、注册到模块导出列表、注册到全局工具注册表、配置技能或团队声明。
第一步:编写工具函数
工具函数必须遵循 agno 框架的规范,第一个参数必须是 run_context: RunContext。函数应通过 _get_ctx(run_context) 获取用户上下文,确保用户身份信息(user_id、lang 等)不会暴露给 LLM。函数返回值必须是 JSON 字符串,便于 LLM 解析。
# app/tools/example_tools.py
from __future__ import annotations
import json
import logging
from agno.run import RunContext
from app.tools.context import _NO_CTX_RESULT, _get_ctx
logger = logging.getLogger(__name__)
async def example_new_tool(
run_context: RunContext,
param1: str,
param2: int = 10,
) -> str:
"""示例工具函数,展示标准工具编写模式。
Args:
param1: 业务参数1,由LLM提供
param2: 业务参数2,可选
Returns:
JSON格式的处理结果
"""
# 1. 获取用户上下文
ctx = _get_ctx(run_context)
if ctx is None:
return _NO_CTX_RESULT
# 2. 执行业务逻辑
try:
# 这里调用 outfit_biz 中的业务函数
# result = await some_business_service(param1, param2, user_id=ctx.user_id)
result = {"message": f"处理 {param1} 完成", "count": param2}
# 3. 返回JSON字符串
return json.dumps(result, ensure_ascii=False)
except Exception as e:
logger.error(f"工具执行失败: {e}")
return json.dumps({"error": str(e)}, ensure_ascii=False)
# 定义模块工具列表
TOOLS = [example_new_tool]
关键要点:
- - 函数签名中不暴露
user_id、lang等会话级参数 - - 使用
_get_ctx(run_context)安全获取用户上下文 - - 返回值必须是 JSON 字符串,便于 LLM 解析
- - 异常处理要完善,返回错误信息而非抛出异常
Sources: app/tools/item_tools.py, app/tools/context.py
第二步:添加工具描述
工具描述存储在 data/tools/descriptions.toml 文件中,用于向 LLM 说明工具的功能、参数和使用场景。描述应清晰、准确,帮助 LLM 正确调用工具。
# data/tools/descriptions.toml
[tools]
example_new_tool = '''
示例工具,用于处理特定业务场景。
适合在用户需要执行某种特定操作时调用。
必须提供 `param1` 参数;可选传入 `param2` 控制处理数量。
返回 JSON 对象,通常包含处理结果和状态信息。
'''
描述编写规范:
- - 使用中文描述,语言简洁明了
- - 明确说明工具的使用场景和时机
- - 列出所有必需参数和可选参数
- - 说明返回值的格式和内容
- - 提供使用示例或注意事项
Sources: data/tools/descriptions.toml
第三步:注册到模块导出列表
在工具模块文件末尾,将工具函数添加到 TOOLS 列表中。这个列表会被上层模块导入并用于构建全局工具注册表。
# app/tools/example_tools.py 文件末尾
async def example_new_tool(...):
# ... 工具实现
# 注册到模块工具列表
TOOLS = [example_new_tool]
如果模块中已有 TOOLS 列表,直接追加即可。确保所有需要暴露给 LLM 的工具函数都包含在列表中。
第四步:注册到全局工具注册表
在 app/tools/__init__.py 中完成以下注册工作:
- 1. 导入工具函数:从新模块导入工具函数
- 2. 导入模块工具列表:导入
TOOLS列表 - 3. 添加到聚合列表:将模块工具添加到
ALL_BIZ_TOOLS - 4. 注册到全局注册表:在
_build_tool_registry()函数中添加映射
# app/tools/__init__.py
from app.tools.example_tools import TOOLS as _EXAMPLE_TOOLS
from app.tools.example_tools import example_new_tool
# 在适当位置添加到聚合列表
ALL_BIZ_TOOLS: list = [
# ... 其他工具
*_EXAMPLE_TOOLS,
]
# 在 _build_tool_registry() 函数中添加映射
def _build_tool_registry() -> dict:
raw_registry = {
# ... 其他工具映射
"example_new_tool": example_new_tool,
}
return {name: build_tool(fn, name=name) for name, fn in raw_registry.items()}
关键点:
- - 导入时使用
_EXAMPLE_TOOLS命名约定,避免命名冲突 - - 确保
ALL_BIZ_TOOLS包含所有业务工具 - - 在
_build_tool_registry()中为每个工具创建映射
Sources: app/tools/__init__.py
第五步:配置技能或团队声明
工具需要通过技能或主团队配置声明才能在运行时被 LLM 调用。
方式一:添加到技能配置
# data/skills/some-skill/skill.toml
id = "some-skill"
name = "示例技能"
tools = [
"example_new_tool",
# 其他技能工具
]
方式二:添加到主团队配置
# data/team/main.toml
id = "outfit-main-team"
tools = [
"example_new_tool",
# 其他常驻工具
]
选择建议:
- - 如果工具属于特定业务场景(如搜品、生图),应添加到对应技能
- - 如果工具是通用基础能力(如天气查询、状态管理),应添加到主团队
- - 避免将所有工具都放在主团队,保持工具的职责边界清晰
Sources: data/skills/example/skill.toml, data/team/main.toml
高级工具特性
工具停止控制
某些工具执行后需要立即停止 Agent 运行,等待用户确认或异步处理完成。可以通过以下方式控制:
# 方式一:静态停止控制
async def stop_after_tool(run_context: RunContext) -> str:
"""执行后立即停止的工具"""
# ... 工具实现
return json.dumps({"status": "completed"})
# 设置工具属性
stop_after_tool.__tool_stop_after_tool_call__ = True
# 方式二:动态停止控制
async def conditional_stop_tool(run_context: RunContext) -> str:
"""根据结果动态决定是否停止的工具"""
result = {"suppress_main_agent": True}
return json.dumps(result)
def _should_stop(result: str) -> bool:
"""判断是否需要停止的条件函数"""
try:
data = json.loads(result)
return data.get("suppress_main_agent", False)
except:
return False
# 设置动态停止条件
conditional_stop_tool.__tool_stop_after_tool_call_if__ = _should_stop
Sources: app/tools/factory.py, app/tools/outfit_workflow_tools.py
数据库依赖注入
对于需要数据库访问的工具,应使用 app/tools/deps.py 中提供的上下文管理器,确保会话生命周期(提交/回滚)的一致管理。
from app.tools.deps import item_repos, outfit_repos
async def tool_with_db(run_context: RunContext) -> str:
"""需要数据库访问的工具"""
ctx = _get_ctx(run_context)
if ctx is None:
return _NO_CTX_RESULT
# 使用 item_repos 获取 ItemRepo 和 ImageRepo
async with item_repos() as (item_repo, image_repo):
# 执行数据库操作
items = await item_repo.get_by_user(ctx.user_id)
# ... 其他操作
# 或使用 outfit_repos 获取更多仓库
async with outfit_repos() as (outfit_repo, item_repo, image_repo):
# ... 穿搭相关操作
return json.dumps({"result": "success"})
可用的依赖注入上下文:
- -
item_repos(): 提供ItemRepo和ImageRepo - -
outfit_repos(): 提供OutfitRepo、ItemRepo和ImageRepo - -
item_repo_only(): 仅提供ItemRepo - -
outfit_repo_only(): 仅提供OutfitRepo - -
image_repo_only(): 仅提供ImageRepo
Sources: app/tools/deps.py
国际化支持
工具函数应支持多语言,通过 ctx.lang 获取用户语言偏好,并使用 get_text() 函数获取本地化文本。
from app.i18n import get_text
async def localized_tool(run_context: RunContext) -> str:
"""支持多语言的工具"""
ctx = _get_ctx(run_context)
if ctx is None:
return _NO_CTX_RESULT
# 获取本地化文本
success_msg = get_text("example_tools", "success_message", lang=ctx.lang)
error_msg = get_text("example_tools", "error_message", lang=ctx.lang)
# ... 业务逻辑
return json.dumps({"message": success_msg})
国际化文件位置:
- -
data/i18n/example_tools.zh.json: 中文文本 - -
data/i18n/example_tools.en.json: 英文文本
工具测试与调试
单元测试
为工具函数编写单元测试,验证功能正确性和边界情况:
# tests/test_example_tools.py
import pytest
from unittest.mock import AsyncMock, MagicMock
from app.tools.example_tools import example_new_tool
@pytest.mark.asyncio
async def test_example_new_tool():
"""测试工具正常执行"""
# 模拟 RunContext
mock_context = MagicMock()
mock_context.session_state = {
"tool_ctx": {
"user_id": 123,
"user_id_str": "123",
"lang": "zh"
}
}
# 执行工具
result = await example_new_tool(mock_context, "test_param")
# 验证结果
assert '"message"' in result
assert '"count"' in result
@pytest.mark.asyncio
async def test_example_new_tool_no_context():
"""测试无上下文时返回错误"""
mock_context = MagicMock()
mock_context.session_state = {}
result = await example_new_tool(mock_context, "test_param")
assert '"error"' in result
集成测试
使用自动化测试框架验证工具在真实环境中的表现:
# tests/test_tool_integration.py
@pytest.mark.integration
async def test_tool_in_agent_flow():
"""测试工具在 Agent 流程中的集成"""
from app.tools import resolve_tools_by_names
# 解析工具
tools = resolve_tools_by_names(["example_new_tool"], owner_id="test")
assert len(tools) == 1
# 验证工具描述
tool = tools[0]
assert tool.name == "example_new_tool"
assert tool.description # 描述不应为空
常见问题与解决方案
工具描述缺失
问题:工具注册后 LLM 无法正确识别或调用。
解决方案:
- 1. 检查
data/tools/descriptions.toml中是否有对应工具描述 - 2. 确认描述格式正确,没有语法错误
- 3. 重启应用或清除工具描述缓存
# 清除缓存
from app.tools.factory import clear_tool_description_cache
clear_tool_description_cache()
工具门控拒绝
问题:工具调用返回 "tool_not_allowed" 错误。
解决方案:
- 1. 检查工具是否在技能或主团队配置中声明
- 2. 确认当前会话的
active_skill是否包含该工具 - 3. 检查渠道权限配置
# 调试门控状态
from app.dialog_state import get_dialog_state, is_tool_allowed
def debug_tool_access(session_state, tool_name):
dialog_state = get_dialog_state(session_state)
active_skill = dialog_state.get("active_skill")
print(f"当前技能: {active_skill}")
print(f"工具是否允许: {is_tool_allowed(session_state, tool_name=tool_name)}")
数据库连接问题
问题:工具执行时数据库连接失败。
解决方案:
- 1. 检查数据库配置是否正确
- 2. 确认依赖注入上下文是否正确使用
- 3. 查看日志中的具体错误信息
# 检查数据库连接
from app.tools.deps import item_repo_only
async def test_db_connection():
try:
async with item_repo_only() as repo:
# 测试简单查询
items = await repo.get_by_user(1)
print(f"连接成功,找到 {len(items)} 条记录")
except Exception as e:
print(f"连接失败: {e}")
最佳实践总结
- 1. 安全性优先:永远不要在工具函数签名中暴露
user_id等敏感参数 - 2. 描述准确:工具描述要清晰、完整,帮助 LLM 正确使用
- 3. 错误处理:返回有意义的错误信息,而不是抛出异常
- 4. 职责单一:每个工具只做一件事,保持工具粒度合理
- 5. 测试覆盖:编写单元测试和集成测试,确保工具可靠性
- 6. 文档同步:及时更新工具描述和相关文档
下一步
完成工具开发后,建议阅读以下文档了解更深入的集成和使用:
- - 工具注册与工厂 - 了解工具注册的底层机制
- - 技能加载与管理 - 学习如何将工具集成到技能系统
- - 技能工具门控机制 - 理解工具访问控制原理
- - 新增技能模块 - 学习创建包含工具的完整技能模块
<a id="31-xin-zeng-ji-neng-mo-kuai"></a>
---
新增技能模块
- - Section: 扩展与定制
- - Source File:
31-xin-zeng-ji-neng-mo-kuai.md
本指南详细说明如何为 Outfit Agent 系统新增一个完整的技能(Skill)模块。技能是系统的核心能力单元,每个技能封装了特定的业务场景、指令模板和工具集合,通过动态加载机制与主 Agent 协同工作。
技能架构概览
Outfit Agent 的技能系统采用声明式配置 + 动态加载架构。技能模块由三个核心部分组成:定义文件(skill.toml)、指令模板(data/prompts/skills/)和工具集合。系统通过 SkillRegistry 自动发现并加载所有技能,运行时根据对话状态动态激活。
graph TB
subgraph "技能定义层"
A[data/skills/<skill-id>/skill.toml] --> B[技能元数据]
A --> C[工具声明]
A --> D[激活模式]
end
subgraph "指令模板层"
E[data/prompts/skills/<skill>.zh.txt] --> F[中文指令]
G[data/prompts/skills/<skill>.en.txt] --> H[英文指令]
end
subgraph "工具注册层"
I[app/tools/__init__.py] --> J[TOOL_REGISTRY]
J --> K[工具实现]
end
subgraph "运行时加载层"
L[app/skills/loader.py] --> M[SkillRegistry]
M --> N[RegisteredSkill]
N --> O[SkillBundle]
end
subgraph "Agent 集成层"
P[data/team/main.toml] --> Q[技能分配]
Q --> R[app/agent.py]
R --> S[Agent 实例]
end
A --> L
E --> L
G --> L
I --> L
L --> R
Sources: loader.py, __init__.py, main.toml
技能目录结构规范
每个技能模块遵循标准的目录结构,支持单文件定义和目录式定义两种方式:
data/skills/
├── <skill-id>/
│ ├── skill.toml # 技能定义文件(必需)
│ ├── references/ # 参考资料目录(可选)
│ │ └── *.txt
│ └── scripts/ # 辅助脚本目录(可选)
│ └── *.py
└── <skill-id>.toml # 单文件定义(轻量级)
目录式定义(推荐):将技能的所有文件组织在独立目录中,便于管理参考资料和脚本。
单文件定义:适用于简单的技能,所有配置集中在一个 TOML 文件中。
Sources: loader.py, example/skill.toml
技能定义文件详解
技能定义文件 skill.toml 是技能的核心配置,包含以下字段:
# 技能唯一标识符(必需)
id = "my-new-skill"
# 技能显示名称(可选,默认使用 id)
name = "我的新技能"
# 技能描述(必需,用于 Agent 理解技能用途)
description = "负责处理特定业务场景的技能模块。"
# 激活模式(可选,默认 "on_demand")
# - "on_demand": 按需激活,Agent 需要时调用 get_skill_instructions
# - "always": 始终激活,指令自动注入 Agent 提示词
activation = "on_demand"
# 版本兼容性声明(可选)
compatibility = "outfit-agent>=1.0"
# 许可证信息(可选)
license = "internal"
# 工具声明(可选,技能可用的工具列表)
tools = [
"tool_name_1",
"tool_name_2",
]
# 允许的工具边界(可选,用于工具门控)
allowed_tools = [
"tool_name_1",
"tool_name_2",
]
# 渠道特定工具覆盖(可选)
[channels.wechat_kefu]
tools = []
[channels.wechat_miniapp]
tools = [
"channel_specific_tool",
]
# 元数据(可选,用于分类和管理)
[metadata]
category = "business"
owner = "main-agent"
# 指令模板路径(必需,相对于 data/prompts/)
[instructions]
zh = "skills/my_new_skill.zh.txt"
en = "skills/my_new_skill.en.txt"
关键字段说明:
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
id | string | 是 | 技能唯一标识符,用于运行时引用 |
name | string | 否 | 技能显示名称,默认使用 id |
description | string | 是 | 技能描述,Agent 用于理解技能用途 |
activation | string | 否 | 激活模式:"on_demand" 或 "always" |
tools | list | 否 | 技能可用的工具名称列表 |
allowed_tools | list | 否 | 工具边界声明,用于门控机制 |
channels | dict | 否 | 渠道特定工具覆盖配置 |
metadata | dict | 否 | 技能元数据,用于分类和管理 |
instructions | dict | 是 | 指令模板路径配置 |
Sources: example/skill.toml, style-advisor-skill/skill.toml, outfit-output-skill/skill.toml
创建新技能的完整步骤
步骤 1:创建技能目录
首先在 data/skills/ 目录下创建技能目录:
mkdir -p data/skills/my-new-skill
步骤 2:创建技能定义文件
在技能目录中创建 skill.toml 文件:
# data/skills/my-new-skill/skill.toml
id = "my-new-skill"
name = "我的新技能"
description = "负责处理特定业务场景的技能模块。"
activation = "on_demand"
compatibility = "outfit-agent>=1.0"
license = "internal"
# 声明技能可用的工具
tools = [
"search_items",
"send_items",
]
# 声明工具边界
allowed_tools = [
"search_items",
"send_items",
]
[metadata]
category = "business"
owner = "main-agent"
[instructions]
zh = "skills/my_new_skill.zh.txt"
en = "skills/my_new_skill.en.txt"
步骤 3:创建指令模板
在 data/prompts/skills/ 目录下创建指令模板文件:
中文指令模板 (data/prompts/skills/my_new_skill.zh.txt):
**[Role-Play] 你是特定业务场景的处理专家。**
## 核心职责
1. 处理用户在特定场景下的需求
2. 提供专业的业务建议和解决方案
3. 确保业务流程的完整性和准确性
## 执行规则
1. 在调用工具前,确保已收集足够的信息
2. 工具调用后,及时更新对话状态
3. 保持回复的专业性和准确性
## 状态管理
1. 使用 `update_dialog_state` 更新业务状态
2. 确保业务流程的正确推进
3. 记录关键业务节点信息
## 输出要求
1. 回复要简洁明了
2. 避免重复已提供的信息
3. 保持专业友好的语气
英文指令模板 (data/prompts/skills/my_new_skill.en.txt):
**[Role-Play] You are an expert in handling specific business scenarios.**
## Core Responsibilities
1. Handle user needs in specific scenarios
2. Provide professional business advice and solutions
3. Ensure completeness and accuracy of business processes
## Execution Rules
1. Ensure sufficient information is collected before calling tools
2. Update dialog state promptly after tool calls
3. Maintain professionalism and accuracy in responses
## State Management
1. Use `update_dialog_state` to update business state
2. Ensure proper progression of business processes
3. Record key business node information
## Output Requirements
1. Keep responses concise and clear
2. Avoid repeating already provided information
3. Maintain a professional and friendly tone
步骤 4:注册技能到主 Agent
在 data/team/main.toml 文件中添加新技能到 skills 列表:
# 主 Agent 可用 skill 列表(来自 data/skills/*)
skills = [
"nuva-soul",
"style-advisor-skill",
"buyer-wardrobe-skill",
"outfit-output-skill",
"my-new-skill", # 添加新技能
]
步骤 5:添加工具描述(如果技能包含新工具)
如果技能包含新工具,需要在 data/tools/descriptions.toml 中添加工具描述:
# data/tools/descriptions.toml
[tools]
my_new_tool = '''
工具的详细描述,包括用途、参数说明和返回值格式。
'''
步骤 6:实现工具(如果技能包含新工具)
如果技能包含新工具,需要在 app/tools/ 目录下实现工具:
# app/tools/my_new_tools.py
from agno.run import RunContext
async def my_new_tool(run_context: RunContext, param1: str, param2: int = 0) -> str:
"""工具的详细文档字符串。"""
# 工具实现逻辑
return '{"result": "success"}'
然后在 app/tools/__init__.py 中注册工具:
# 在 _build_tool_registry 函数中添加
raw_registry = {
# ... 现有工具
"my_new_tool": my_new_tool, # 添加新工具
}
步骤 7:测试技能
创建测试文件验证技能功能:
# tests/test_my_new_skill.py
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def test_my_new_skill_files_exist():
skill_toml = ROOT / "data/skills/my-new-skill/skill.toml"
assert skill_toml.exists()
skill_prompt_zh = ROOT / "data/prompts/skills/my_new_skill.zh.txt"
assert skill_prompt_zh.exists()
Sources: loader.py, access_tools.py, factory.py
技能激活模式详解
系统支持两种技能激活模式,影响技能指令的加载方式:
1. 按需激活模式 (on_demand)
按需激活模式下,技能指令不会自动注入 Agent 提示词。Agent 需要主动调用 get_skill_instructions 工具来加载技能指令。
适用场景:
- - 特定业务场景的技能
- - 需要明确边界控制的技能
- - 按需加载的专家技能
工作流程:
- 1. Agent 识别到需要特定技能的场景
- 2. 调用
get_skill_instructions(skill_name="my-new-skill") - 3. 系统加载技能指令并激活技能状态
- 4. Agent 执行技能相关操作
2. 始终激活模式 (always)
始终激活模式下,技能指令会自动注入 Agent 提示词,无需手动加载。
适用场景:
- - 通用业务规则
- - 始终生效的约束条件
- - 基础行为准则
配置示例:
# data/skills/my-always-skill/skill.toml
id = "my-always-skill"
activation = "always"
Sources: loader.py, loader.py, dialog_state.py
工具门控机制
系统通过工具门控机制确保工具只能在正确的技能上下文中使用:
工具所有权声明
在技能定义中声明工具所有权:
# data/skills/my-new-skill/skill.toml
tools = [
"search_items",
"send_items",
]
allowed_tools = [
"search_items",
"send_items",
]
门控逻辑
工具调用时,系统会检查:
- 1. 工具是否在
ALWAYS_ALLOWED_TOOLS中(如get_skill_instructions) - 2. 工具是否有技能所有权声明
- 3. 当前激活的技能是否匹配工具所有权
门控错误响应:
{
"error": "tool_not_available",
"tool_name": "search_items",
"required_skill": ["my-new-skill"],
"message": "Tool 'search_items' requires activating one of: my-new-skill"
}
Sources: dialog_state.py, dialog_state.py, factory.py
渠道特定工具覆盖
系统支持为不同消息渠道配置不同的工具集合:
配置示例
# data/skills/my-new-skill/skill.toml
tools = [
"search_items",
"send_items",
]
# 微信客服渠道特定配置
[channels.wechat_kefu]
tools = []
# 微信小程序渠道特定配置
[channels.wechat_miniapp]
tools = [
"edit_outfit",
]
工具解析逻辑
- 1. 基础工具列表:
tools字段定义的工具 - 2. 渠道特定工具:
channels.<channel_name>.tools定义的工具 - 3. 最终工具列表:基础工具 + 渠道特定工具(去重)
Sources: loader.py, loader.py, outfit-output-skill/skill.toml
参考资料和脚本管理
技能可以包含参考资料和辅助脚本,用于提供额外的业务知识:
目录结构
data/skills/my-new-skill/
├── skill.toml
├── references/
│ ├── style_guide.txt
│ └── business_rules.md
└── scripts/
├── data_processor.py
└── validator.py
访问方式
系统提供专用工具访问技能的参考资料和脚本:
- 1.
get_skill_reference:读取技能的参考资料
```python get_skill_reference(skill_name="my-new-skill", reference_name="style_guide.txt") ```
- 2.
get_skill_script:读取技能的辅助脚本
```python get_skill_script(skill_name="my-new-skill", script_name="data_processor.py") ```
Sources: access_tools.py, loader.py
灰度发布支持
系统支持基于用户的灰度发布,允许为不同用户配置不同的技能版本:
灰度配置
技能配置可以通过灰度服务进行覆盖:
# 灰度服务会根据用户 ID 加载对应的技能配置
skill_cfg = get_gray_config_service().get_toml_overlay(
"skill",
"skills/my-new-skill/skill.toml",
user_id=user_id,
base_content=raw_text,
)
灰度范围
系统会为每个技能定义生成灰度范围路径:
# 生成灰度签名
scopes = [f"skill:{path}" for path in self._discover()]
signature = get_gray_config_service().build_gray_signature(
user_id=user_id,
scopes=scopes,
)
测试与验证
单元测试
创建测试文件验证技能功能:
# tests/test_my_new_skill.py
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def test_skill_toml_structure():
"""验证技能定义文件结构。"""
skill_toml = ROOT / "data/skills/my-new-skill/skill.toml"
assert skill_toml.exists()
import tomllib
with open(skill_toml, "rb") as f:
cfg = tomllib.load(f)
assert "id" in cfg
assert "name" in cfg
assert "description" in cfg
assert "instructions" in cfg
def test_skill_prompt_files():
"""验证技能指令文件存在。"""
prompt_zh = ROOT / "data/prompts/skills/my_new_skill.zh.txt"
prompt_en = ROOT / "data/prompts/skills/my_new_skill.en.txt"
assert prompt_zh.exists()
assert prompt_en.exists()
def test_skill_in_main_team():
"""验证技能已注册到主 Agent。"""
main_toml = ROOT / "data/team/main.toml"
import tomllib
with open(main_toml, "rb") as f:
cfg = tomllib.load(f)
assert "my-new-skill" in cfg.get("skills", [])
集成测试
验证技能在运行时的正确加载和激活:
# tests/test_skill_integration.py
def test_skill_loading():
"""验证技能正确加载。"""
from app.skills import get_skill_registry
registry = get_skill_registry()
skill = registry.get_skill("my-new-skill")
assert skill is not None
assert skill.id == "my-new-skill"
assert skill.name == "我的新技能"
Sources: test_persona_skill_contract.py, loader.py
最佳实践
1. 技能设计原则
- - 单一职责:每个技能专注于一个明确的业务场景
- - 清晰边界:明确定义技能的职责范围和限制
- - 可组合性:技能应能与其他技能协同工作
- - 可测试性:技能应易于单元测试和集成测试
2. 指令模板编写
- - 明确角色:清晰定义技能的角色和职责
- - 执行规则:提供明确的执行步骤和规则
- - 状态管理:说明如何与对话状态交互
- - 输出要求:定义输出格式和质量要求
3. 工具设计
- - 最小权限:只暴露必要的工具
- - 清晰接口:提供明确的参数和返回值说明
- - 错误处理:提供友好的错误信息和恢复建议
- - 性能考虑:避免阻塞操作,提供异步支持
4. 版本管理
- - 兼容性声明:使用
compatibility字段声明版本要求 - - 灰度发布:利用灰度服务进行渐进式发布
- - 向后兼容:保持技能接口的向后兼容性
Sources: loader.py, access_tools.py, factory.py
故障排除
常见问题
- 1. 技能未加载
- - 检查
skill.toml文件是否存在且格式正确 - - 确认
enable字段未设为false - - 查看日志中的错误信息
- 2. 工具调用失败
- - 检查工具是否在
tools列表中声明 - - 确认工具已在
TOOL_REGISTRY中注册 - - 验证技能是否已正确激活
- 3. 指令模板未加载
- - 检查指令文件路径是否正确
- - 确认文件编码为 UTF-8
- - 验证
[instructions]配置是否正确
- 4. 渠道特定工具不生效
- - 检查渠道名称是否正确
- - 确认当前会话的渠道标识
- - 验证工具是否在正确的渠道配置中
调试技巧
- 1. 启用调试日志:
```python import logging logging.getLogger('app.skills').setLevel(logging.DEBUG) ```
- 2. 检查技能注册状态:
```python from app.skills import get_skill_registry registry = get_skill_registry() print(f"已加载技能: {list(registry.skills.keys())}") ```
- 3. 验证工具注册:
```python from app.tools import TOOL_REGISTRY print(f"已注册工具: {list(TOOL_REGISTRY.keys())}") ```
Sources: loader.py, __init__.py, factory.py
下一步
完成技能模块开发后,建议继续阅读以下文档:
- - 技能加载与管理:深入了解技能加载机制和生命周期管理
- - 技能工具门控机制:掌握工具门控的详细实现和配置
- - 工具注册与工厂:学习如何创建和注册自定义工具
- - 提示词管理与国际化:掌握指令模板的国际化和版本管理
- - 单元测试与集成测试:学习如何为技能编写全面的测试用例
<a id="32-ti-shi-ci-guan-li-yu-guo-ji-hua"></a>
---
提示词管理与国际化
- - Section: 扩展与定制
- - Source File:
32-ti-shi-ci-guan-li-yu-guo-ji-hua.md
本文档详细介绍了 outfit_agent 项目中的提示词(Prompt)管理系统和国际化(i18n)架构。该系统采用分层设计,支持多语言、多渠道的提示词管理,并通过管理后台实现动态配置与版本控制。
系统架构概述
outfit_agent 的提示词管理系统采用三层架构设计,实现了业务逻辑与内容的分离:
- 1. 数据层:存储所有提示词文本和国际化字符串
- 2. 服务层:提供统一的加载、缓存和查询接口
- 3. 应用层:各业务模块通过标准化接口获取提示词
graph TB
subgraph "数据层"
A[outfit_agent/data]
B[outfit_biz/data]
end
subgraph "服务层"
C[I18nLoader]
D[PromptLoader]
E[GrayConfigService]
end
subgraph "应用层"
F[Agent 模块]
G[Skill 模块]
H[工具模块]
I[消息模块]
end
A --> C
B --> C
C --> D
C --> E
D --> F
D --> G
D --> H
D --> I
E --> F
E --> G
E --> H
E --> I
核心设计原则:
- - 分层覆盖:agent 目录优先,biz 目录作为后备
- - 语言回退:先查找语言特定文件,再查找默认文件
- - 缓存机制:内存缓存减少 I/O 开销
- - 动态覆盖:支持通过灰度配置动态修改提示词内容
Sources: i18n.py, prompt_loader.py
目录结构与文件组织
提示词和国际化文件按照功能分类存储在 data/ 目录下:
data/
├── i18n/ # 国际化字符串(JSON 格式)
│ ├── system.zh.json # 系统级字符串
│ ├── weather.en.json # 天气相关字符串
│ └── ...
├── prompts/ # 提示词文本文件
│ ├── agents/ # Agent 提示词
│ ├── skills/ # Skill 提示词
│ ├── memory/ # 记忆相关提示词
│ ├── system/ # 系统提示词
│ └── workflows/ # 工作流提示词
├── channels/ # 渠道专属提示词
│ ├── wechat_kefu/ # 微信客服渠道
│ └── wechat_miniapp/ # 微信小程序渠道
└── skills/ # 技能配置文件
├── nuva-soul/
├── style-advisor-skill/
└── ...
文件命名规范:
- - 语言后缀:
<name>.<lang>.<ext>(如main_team.zh.txt) - - 基础文件:
<name>.<ext>(作为语言回退) - - 目录结构:反映功能分类和模块归属
国际化(i18n)系统
核心组件
国际化系统由 I18nLoader 类提供核心功能,支持两种加载模式:
- 1. 短字符串模式:从 JSON 文件加载键值对
- 2. 文本文件模式:加载完整的文本文件内容
classDiagram
class I18nLoader {
-_search_dirs: list[Path]
-_json_cache: dict
-_file_cache: dict
-_lock: threading.Lock
+register_search_dir(directory, prepend)
+get(name, key, lang, default)
+load_file(relative_path, lang)
+reload_json(name, lang)
+reload_file(relative_path, lang)
+clear_cache()
}
class PromptLoader {
+load(relative_path, lang)
+reload(relative_path, lang)
+clear_cache()
}
I18nLoader <|-- PromptLoader
多路径搜索机制
系统支持多个搜索目录,按优先级顺序查找:
- 1. agent 数据目录(最高优先级)
- 2. biz 数据目录(后备)
这种设计允许 agent 层覆盖 biz 层的文件,无需符号链接或文件复制。
# 搜索目录注册示例
from outfit_biz.utils.i18n import register_search_dir
# 注册 agent 数据目录(高优先级)
register_search_dir(agent_data_dir, prepend=True)
# 注册 biz 数据目录(低优先级)
register_search_dir(biz_data_dir, prepend=False)
语言解析规则
对于每个资源文件,系统按以下顺序查找:
- 1. 语言特定文件:
<stem>.<lang><ext>(如main_team.en.txt) - 2. 基础文件:
<stem><ext>(如main_team.txt)
示例:
- -
get_prompt("agents/main_team.txt", lang="en")查找:
- 1.
prompts/agents/main_team.en.txt - 2.
prompts/agents/main_team.txt
Sources: i18n.py
提示词管理接口
统一访问接口
系统提供三个核心函数,分别用于不同场景:
| 函数 | 用途 | 路径基准 | 示例 |
|---|---|---|---|
get_text() | 获取短字符串 | data/i18n/ | get_text("weather", "comfort.hot") |
get_file() | 获取文本文件 | data/ | get_file("prompts/image_caption.txt") |
get_prompt() | 获取提示词 | data/prompts/ | get_prompt("agents/main_team.txt") |
使用示例:
from app.i18n import get_text, get_file, get_prompt
# 获取国际化字符串
label = get_text("weather", "comfort.hot", lang="zh") # "炎热"
# 获取提示词文件
prompt = get_prompt("agents/main_team.txt", lang="en")
# 获取任意文本文件
content = get_file("fast_response_text.txt", lang="zh")
Sources: i18n.py
动态占位符解析
提示词支持动态占位符,在运行时解析:
def resolve_prompt_placeholders(text: str, lang: str = DEFAULT_LANG) -> str:
"""解析提示词中的动态占位符"""
resolved = text
if "{category_taxonomy_yaml}" in resolved:
resolved = resolved.replace("{category_taxonomy_yaml}", build_category_yaml())
if "{category_taxonomy}" in resolved:
resolved = resolved.replace("{category_taxonomy}", build_category_prompt(lang=lang))
return resolved
支持的占位符:
- -
{category_taxonomy_yaml}:分类体系 YAML 格式 - -
{category_taxonomy}:分类体系提示词格式 - -
{tone_setting}:用户个性化语调设置
Sources: i18n.py
渠道专属提示词
渠道覆盖机制
系统支持为不同渠道(如微信客服、小程序)提供专属提示词:
graph LR
A[基础提示词] --> B{渠道检查}
B -->|有渠道覆盖| C[合并渠道提示词]
B -->|无渠道覆盖| D[使用基础提示词]
C --> E[最终提示词]
D --> E
目录结构:
data/channels/
├── wechat_kefu/
│ ├── agents/main_team.zh.txt
│ ├── outfit_summary.zh.txt
│ └── skills/
└── wechat_miniapp/
├── agents/main_team.zh.txt
└── ...
实现逻辑:
def load_optional_channel_prompt(
relative_path: str,
*,
channel: object | None,
lang: str = DEFAULT_LANG,
user_id: int | str | None = None,
) -> str:
"""加载渠道专属提示词"""
path = build_channel_prompt_path(relative_path, channel=channel)
if not path:
return ""
try:
return get_file(path, lang=lang, user_id=user_id).strip()
except FileNotFoundError:
return ""
Sources: channel_prompts.py, channels目录
渠道上下文解析
系统从会话状态中提取渠道和语言信息:
def resolve_prompt_lang(session_state: Mapping[str, Any] | None, *, default: str = DEFAULT_LANG) -> str:
"""从会话状态解析语言设置"""
tool_ctx = resolve_tool_ctx(session_state)
if tool_ctx is not None and hasattr(tool_ctx, "lang"):
return tool_ctx.lang or default
return default
def resolve_channel(session_state: Mapping[str, Any] | None) -> str | None:
"""从会话状态解析渠道信息"""
tool_ctx = resolve_tool_ctx(session_state)
if tool_ctx is not None and hasattr(tool_ctx, "channel"):
return normalize_channel_name(tool_ctx.channel)
return None
Sources: channel_prompts.py
Agent 提示词构建
主团队提示词构建流程
主团队(Main Team)的提示词构建遵循严格的流程:
sequenceDiagram
participant App as 应用层
participant Loader as 提示词加载器
participant I18n as 国际化系统
participant Channel as 渠道处理器
App->>Loader: 请求主团队提示词
Loader->>I18n: 获取基础提示词
I18n-->>Loader: 返回基础提示词
Loader->>Loader: 替换 {tone_setting}
Loader->>Channel: 加载渠道专属提示词
Channel-->>Loader: 返回渠道提示词
Loader->>Loader: 合并所有部分
Loader-->>App: 返回最终提示词列表
核心函数:
def resolve_main_team_instructions(
team_cfg: Any,
*,
lang: str = DEFAULT_LANG,
tone_setting: str | None = None,
channel: str | None = None,
user_id: int | str | None = None,
# ... 其他参数
) -> list[str]:
# 1. 获取基础提示词
prompt_text = _resolve_main_team_prompt_text(team_cfg, lang=lang, channel=channel, user_id=user_id)
# 2. 替换语调设置
resolved = prompt_text.replace("{tone_setting}", resolve_tone_setting(tone_setting, lang=lang))
# 3. 合并渠道提示词和工具护栏
return merge_instruction_sections(
resolved,
_resolve_always_skill_prompt_texts(...),
_resolve_channel_prompt_text(...),
build_tool_runtime_guardrail(tool_names, lang=lang),
)
Sources: main_team_prompt.py
工具运行时护栏
系统为工具调用提供运行时护栏提示词:
def build_tool_runtime_guardrail(
tool_names: Sequence[str] | None,
*,
lang: str = DEFAULT_LANG,
) -> str:
"""构建工具运行时护栏提示词"""
template = get_prompt("partials/tool_runtime_guardrail.txt", lang=lang).strip()
if not template:
return ""
# 格式化可用工具列表
tools_str = _format_tool_names(tool_names, lang=lang)
return template.format(available_tools=tools_str).strip()
工具列表格式化:
def _format_tool_names(tool_names: Sequence[str] | None, *, lang: str) -> str:
names = _normalize_tool_names(tool_names)
if not names:
empty_label = "(当前无可用工具)" if lang == "zh" else "(no tools available in this session)"
return f"- {empty_label}"
return "\n".join(f"- `{name}`" for name in names)
Sources: tool_runtime_prompt.py
灰度配置与动态覆盖
灰度配置服务
系统通过 GrayConfigService 实现提示词的动态覆盖:
graph TB
A[用户请求] --> B{灰度规则匹配}
B -->|匹配| C[应用覆盖内容]
B -->|不匹配| D[使用基础内容]
C --> E[返回最终内容]
D --> E
覆盖类型:
- 1. 提示词覆盖:修改 Agent 提示词内容
- 2. JSON 覆盖:合并或替换 JSON 配置
- 3. 文本覆盖:修改短字符串内容
- 4. 模型补丁:动态修改模型参数
实现示例:
def get_prompt_overlay(
self,
path: str,
*,
lang: str,
user_id: int | str | None,
base_content: str,
) -> str:
"""获取提示词覆盖内容"""
return self._apply_text_namespace("prompt", path, lang=lang, user_id=user_id, base_content=base_content)
Sources: gray/service.py
用户特定覆盖
系统支持基于用户 ID 的个性化覆盖:
def get_text(
name: str,
key: str,
lang: str = DEFAULT_LANG,
*,
default: str = "",
user_id: int | str | None = None,
) -> str:
"""获取国际化字符串,支持用户特定覆盖"""
for data_dir in self._search_dirs:
try:
mapping = self._load_json_from(data_dir, name, lang)
value = mapping.get(key, "__MISS__")
if value != "__MISS__":
# 应用灰度配置覆盖
return get_gray_config_service().get_text_overlay(
f"{name}.{key}",
lang=lang,
user_id=user_id,
base_value=value,
)
except FileNotFoundError:
continue
# 未找到时也应用灰度配置
return get_gray_config_service().get_text_overlay(
f"{name}.{key}",
lang=lang,
user_id=user_id,
base_value=default,
)
Sources: i18n.py
管理后台接口
资源管理 API
管理后台提供完整的提示词管理功能:
| 端点 | 方法 | 功能 |
|---|---|---|
/api/admin/prompts/list | GET | 列出所有资源文件 |
/api/admin/prompts/content | GET | 获取资源内容 |
/api/admin/prompts/content | PUT | 更新资源内容 |
/api/admin/prompts/clear-cache | POST | 清除缓存 |
/api/admin/prompts/history | GET | 获取版本历史 |
/api/admin/prompts/rollback | POST | 回滚到指定版本 |
资源发现机制:
def _discover_resources() -> list[dict]:
"""发现所有资源文件"""
resources = []
by_rel = {}
for source, base_dir in _data_roots().items():
if not base_dir.is_dir():
continue
for file_path in sorted(_iter_data_files(base_dir)):
rel_path = file_path.relative_to(base_dir).as_posix()
by_rel.setdefault(rel_path, set()).add(source)
resources.append(_build_resource(source, rel_path, file_path))
# 标记覆盖关系
for item in resources:
peers = by_rel.get(item["path"], set())
item["overlay"] = (
"overrides" if item["source"] == "agent" and "biz" in peers
else "shadowed" if item["source"] == "biz" and "agent" in peers
else None
)
return resources
Sources: prompts.py
版本控制
系统为可编辑资源提供轻量级版本控制:
def _backup(resource: dict) -> None:
"""备份资源文件"""
history_dir = _history_dir_for(resource)
history_dir.mkdir(parents=True, exist_ok=True)
# 生成时间戳版本号
version = str(int(time.time()))
history_file = history_dir / f"{version}.txt"
# 复制当前文件到历史目录
shutil.copy2(resource["full_path"], history_file)
# 保留最近 N 个版本
versions = sorted(history_dir.glob("*"), reverse=True)
for old_version in versions[_MAX_HISTORY_VERSIONS:]:
old_version.unlink()
Sources: prompts.py
Skill 提示词管理
Skill 配置结构
每个 Skill 通过 skill.toml 文件定义:
# data/skills/nuva-soul/skill.toml
id = "nuva-soul"
name = "Nuva Soul"
description = "Nuva 的前台人格与护短表达层"
activation = "on_demand"
compatibility = "outfit-agent>=1.0"
license = "internal"
[metadata]
category = "persona"
owner = "main-team"
[instructions]
zh = "skills/nuva_soul.zh.txt"
en = "skills/nuva_soul.en.txt"
Skill 提示词加载:
def get_skill_instructions(skill_name: str, lang: str = DEFAULT_LANG) -> str:
"""获取 Skill 指令"""
skill_config = load_skill_config(skill_name)
prompt_path = skill_config.instructions.get(lang) or skill_config.instructions.get(DEFAULT_LANG)
return get_prompt(prompt_path, lang=lang)
Sources: skill.toml
最佳实践与开发指南
新增提示词步骤
- 1. 确定位置:根据功能选择
data/prompts/下的子目录 - 2. 创建文件:遵循命名规范
<name>.<lang>.txt - 3. 添加回退:创建无语言后缀的基础文件
- 4. 注册配置:在相应的 TOML 配置中引用
- 5. 测试验证:确保多语言回退正常工作
国际化字符串管理
- 1. JSON 结构:使用扁平键值对,支持点分隔命名空间
- 2. 语言覆盖:优先使用语言特定文件
- 3. 默认值:确保所有键都有合理的默认值
- 4. 缓存管理:修改后清除缓存或重启服务
性能优化建议
- 1. 缓存利用:充分利用内存缓存减少 I/O
- 2. 懒加载:按需加载大型提示词文件
- 3. 批量操作:管理后台支持批量更新
- 4. 监控告警:监控缓存命中率和加载延迟
故障排除
常见问题
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 提示词未更新 | 缓存未清除 | 调用 clear-cache API |
| 语言回退失败 | 文件命名错误 | 检查文件命名规范 |
| 渠道提示词不生效 | 路径配置错误 | 验证渠道目录结构 |
| 灰度配置不生效 | 规则匹配失败 | 检查用户 ID 和规则条件 |
调试工具
- 1. 管理后台:实时查看和编辑提示词
- 2. 日志记录:详细的加载和缓存日志
- 3. 单元测试:覆盖各种边界情况
- 4. 性能监控:缓存命中率和响应时间
相关文档
- - 单主 Agent 架构:了解 Agent 如何使用提示词
- - 技能加载与管理:Skill 系统的详细设计
- - 工具注册与工厂:工具系统的集成方式
- - Dashboard 管理面板:管理后台的使用指南
- - 新增技能模块:如何创建新的 Skill