browse.nuvatech.cn Wiki

browse.nuvatech.cn

browse.nuvatech.cn Wiki 导出

目录

<a id="1-xiang-mu-gai-lan"></a>

---

项目概览

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.ymlmodel_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"

技能支持以下特性:

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. 1. 入站处理:验证、审计、快捷回复检测
  2. 2. 会话分发:根据会话状态决定处理方式
  3. 3. Agent 调用:将消息发送给 Agent 处理
  4. 4. 回复格式化:根据渠道特性格式化回复
  5. 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. 1. 任务创建:创建穿搭生成任务,返回任务 ID
  2. 2. 后台执行:工作流服务异步执行多步骤流程
  3. 3. 状态更新:通过 WebSocket 或轮询向客户端推送进度
  4. 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',

    // ...

  }]

}

部署依赖:

Sources: ecosystem.config.js, app/main.py

测试体系

项目包含完整的测试体系,覆盖单元测试、集成测试和自动化测试:


# 运行测试

pytest



# 运行特定测试

pytest tests/test_agentos.py

测试文件位于 tests/ 目录,共 103 个测试文件,覆盖系统各个模块。

Sources: tests/, pyproject.toml

开发规范

项目遵循严格的开发规范:

代码风格

提示词管理

Agent 实例管理

配置管理

Sources: AGENTS.md, pyproject.toml

下一步阅读

根据您的开发需求,建议按以下顺序阅读文档:

快速入门

架构理解

核心功能

扩展开发

<a id="2-huan-jing-da-jian-yu-qi-dong"></a>

---

环境搭建与启动

本页面指导开发者从零开始搭建 outfit_agent 的本地开发/部署环境,涵盖依赖安装、配置文件设置、数据库初始化及服务启动的完整流程。

一、前置条件

在开始之前,请确保系统已安装以下基础组件:

组件最低版本用途安装方式
Python3.11+核心运行时python.org
uv最新版Python 包管理与运行`curl -LsSf https://astral.sh/uv/install.sh \sh`
PostgreSQL14+(推荐 16)主数据库(含向量检索扩展)postgresql.org
Redis6.0+缓存/任务队列redis.io
Node.js18+(可选)Dashboard 前端构建nodejs.org
Git最新版代码管理git-scm.com
关键数据库扩展:PostgreSQL 需安装 pgvectorpgvectorscalepg_searchzhparser 扩展,用于向量检索与中文全文搜索。

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.tomloutfit-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. 1. 读取 pyproject.tomluv.lock 中的依赖声明
  2. 2. 创建 .venv 虚拟环境
  3. 3. 安装 outfit_agent 自身及 outfit_biz(editable 模式)
  4. 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

该脚本会创建以下核心表:

注意:首次运行应用时,app/main.py 的 lifespan 会自动执行增量迁移(ALTER TABLE ... ADD COLUMN IF NOT EXISTS),因此即使 init.sql 版本略有滞后,应用也能自动补齐缺失字段。

Sources: init.sql | main.py

六、配置环境变量

6.1 创建 .env 文件


cp .env.example .env

6.2 必选配置项

以下配置项为服务正常运行的最低要求:

配置项说明示例值
OPENAI_API_KEYLLM 服务 API Key(或兼容网关 Key)sk-xxxx
DATABASE_URLPostgreSQL 连接 URLpostgresql+asyncpg://postgres:postgres@localhost:5432/outfit_biz
REDIS_URLRedis 连接地址redis://localhost:6379/0

6.3 推荐配置项

配置项说明默认值
OPENAI_BASE_URLLLM 网关地址(如使用代理)空(使用官方地址)
DASHSCOPE_API_KEY阿里云 DashScope API Key(视觉/Embedding 模型)
MEM0_ENABLED是否启用 Mem0 长期记忆true
ADMIN_SECRET_KEYAdmin 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. 1. 环境变量 / .env
  2. 2. config.yml 合并值
  3. 3. 代码中的默认值

7.2 关键配置节

配置节说明配置文件
model_mapLLM 模型映射(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):

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_TOKENDEV_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

Sources: main.py | README.md

十、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 syncoutfit_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: pgvectorpg_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. 1. 环境变量配置 — 详细了解每个配置项的用途和调优建议
  2. 2. 配置文件结构 — 深入理解 config.yml 的合并策略和完整字段
  3. 3. 系统架构总览 — 从全局视角理解系统各组件的协作关系
  4. 4. 单元测试与集成测试 — 学习如何编写和运行测试
  5. 5. 新增业务工具 — 扩展 Agent 能力的第一步

<a id="3-huan-jing-bian-liang-pei-zhi"></a>

---

环境变量配置

本文档详细说明 outfit_agent 项目中所有环境变量的配置方式、用途和优先级规则。环境变量是系统配置的最高优先级来源,掌握这些变量的正确配置对于项目的正常运行至关重要。

配置加载机制

outfit_agent 的配置系统采用多层叠加架构,优先级从高到低依次为:

  1. 1. 环境变量(最高优先级):直接在操作系统或 .env 文件中设置
  2. 2. .env 文件:项目根目录下的环境变量文件
  3. 3. config.yml 配置文件:outfit_agent 和 outfit_biz 两个项目的 YAML 配置叠加
  4. 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.pyoutfit_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_MAPTrue禁用 LiteLLM 从 GitHub 拉取模型价格表
LITELLM_REDIS_CACHE_ENABLEDfalse启用 LiteLLM Redis LLM 响应缓存
LITELLM_REDIS_TTL3600LiteLLM 缓存 TTL(秒)

LLM 配置推荐统一走 OpenAI 兼容网关,模型别名和默认参数在 config.ymlmodel_map 节配置。

Sources: .env.example, app/config.py

服务配置

变量名默认值说明必填
HOST0.0.0.0服务监听地址
PORT8000服务监听端口
LOG_LEVELinfo日志级别(DEBUG/INFO/WARNING/ERROR/CRITICAL)
AGENT_NAMEoutfit-agentAgent 实例名称
DEBUGfalse调试模式开关

Sources: .env.example, app/config.py

数据库与缓存配置

变量名默认值说明必填
DATABASE_URLpostgresql+asyncpg://postgres:postgres@localhost:5432/outfitPostgreSQL 数据库连接 URL
DATABASE_POOL_SIZE10数据库连接池大小
DATABASE_MAX_OVERFLOW20连接池最大溢出数
REDIS_URLredis://localhost:6379/0Redis 连接 URL
MEM0_ENABLEDtrue启用 Mem0 记忆组件
MEM0_REALTIME_ANALYSIS_ENABLEDfalse启用每条对话实时 mem0 提取/写入
MEM0_COLLECTION_NAMEoutfit_memMem0 集合名称
MEM0_SEARCH_LIMIT10Mem0 搜索结果数量限制
MEM0_HISTORY_DB_PATHdata/mem0_history.dbmem0 历史记录 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_PAGEPATHpages/index/index小程序卡片跳转路径
WECHAT_MINIPROGRAM_IMAGE_URL-小程序卡片缩略图 URL
MESSAGE_ACCESS_CONTROL_ENABLEDtrue启用消息访问控制

企业微信客服配置用于接收客服消息回调。小程序配置用于发送小程序卡片消息。

Sources: .env.example, app/config.py

阿里云内容安全配置

变量名默认值说明必填
ALIYUN_AK_ID-阿里云 AccessKey ID
ALIYUN_AK_SECRET-阿里云 AccessKey Secret
ALIYUN_CONTENT_AUDIT_ENABLEDtrue启用内容安全审核
ALIYUN_GREEN_ENDPOINTgreen-cip.cn-shanghai.aliyuncs.comGreen SDK 端点
ALIYUN_GREEN_REGIONcn-shanghaiGreen SDK 区域
ALIYUN_GREEN_CONNECT_TIMEOUT_MS3000连接超时(毫秒)
ALIYUN_GREEN_READ_TIMEOUT_MS6000读取超时(毫秒)
ALIYUN_TEXT_AUDIT_ENABLEDtrue启用文本审核
ALIYUN_TEXT_AUDIT_SERVICEcomment_detection_pro文本审核服务
ALIYUN_TEXT_AUDIT_BLOCKING_RISK_LEVELShigh文本审核阻断风险等级
ALIYUN_IMAGE_AUDIT_ENABLEDtrue启用图片审核
ALIYUN_IMAGE_AUDIT_SERVICEbaselineCheck图片审核服务
ALIYUN_IMAGE_AUDIT_BLOCKING_RISK_LEVELShigh图片审核阻断风险等级

阿里云内容安全用于对用户发送的文本和图片进行合规性审核。

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_PATHdata/ed25519-private.pemEd25519 私钥文件路径

和风天气配置用于提供穿搭场景的天气查询能力。私钥文件需要从和风天气控制台生成。

Sources: .env.example, app/config.py, app/weather/client.py

图片生成配置

变量名默认值说明必填
IMAGE_GEN_QUALITY2KAI 生图质量(1K/2K/4K)
IMAGE_GEN_PROVIDERgemini生图提供商
IMAGE_GEN_PROVIDERS-逗号分隔的提供商优先级列表
GEMINI_API_KEY-Gemini API 密钥
GEMINI_IMAGE_MODELgemini-2.0-flash-preview-image-generationGemini 生图模型
OPENAI_IMAGE_API_KEY-OpenAI 生图 API 密钥
OPENAI_IMAGE_MODELgpt-image-1OpenAI 生图模型
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_ID1开发环境默认用户 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_ENDPOINThttps://in-otel.hyperdx.ioHyperDX 端点
OTEL_EXPORTER_OTLP_ENDPOINT-OpenTelemetry OTLP 端点
OTEL_SERVICE_VERSION-服务版本号

HyperDX 监控通过 OpenTelemetry 协议发送日志数据。配置 HYPERDX_API_KEYOTEL_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_SECONDS3600签名过期时间(秒)

对象存储用于存储用户上传的图片和 AI 生成的穿搭图。

Sources: outfit_biz/src/outfit_biz/config.py

穿搭工作流配置

变量名默认值说明必填
IMG_GEN_MAX_CONCURRENCY3图片生成最大并发数
IMG_GEN_QUEUE_POP_TIMEOUT_SECONDS1队列弹出超时(秒)
IMG_GEN_DB_RECONCILE_INTERVAL_SECONDS180数据库对账间隔(秒)
IMG_GEN_DB_BATCH_SIZE100数据库对账批次大小
IMG_GEN_QUEUE_QUEUED_TTL_SECONDS600队列中任务 TTL(秒)
IMG_GEN_PROCESSING_LOCK_TTL_SECONDS1800处理锁 TTL(秒)
LLM_CHAT_TIMEOUT_SECONDS600LLM 聊天超时(秒)
LLM_CHAT_MAX_RETRIES0LLM 聊天最大重试次数
ITEM_INIT_MAX_CONCURRENCY2单品初始化最大并发数
IMAGE_CAPTION_MAX_CONCURRENCY2图片描述最大并发数

这些配置控制穿搭工作流的异步处理行为,包括图片生成、单品初始化和图片描述等后台任务。

Sources: app/config.py, app/main.py

用户洞察配置

变量名默认值说明必填
INSIGHT_OUTFIT_DELTA_THRESHOLD20穿搭洞察触发阈值
INSIGHT_ITEM_DELTA_THRESHOLD20单品洞察触发阈值
INSIGHT_CHAT_INTERVAL_DAYS30对话洞察间隔天数
INSIGHT_MERGE_HISTORY_DAYS730洞察合并历史天数
INSIGHT_TONE_HISTORY_DAYS730语气分析历史天数
INSIGHT_CHAT_OVERLAP_MESSAGES5对话重叠消息数
INSIGHT_SCHEDULER_CRON0 0 *洞察调度 Cron 表达式
INSIGHT_SCHEDULER_TIMEZONEAsia/Shanghai调度时区
INSIGHT_BATCH_SIZE200批量处理大小
INSIGHT_USER_CONCURRENCY5用户处理并发数

用户洞察是离线任务,每天定时分析用户的穿搭记录、衣橱单品和对话记录,生成用户画像并同步到 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. 1. outfit_biz/config.yml 是基础配置
  2. 2. outfit_agent/config.yml 在基础上叠加(deep-merge)
  3. 3. 环境变量具有最高优先级,会覆盖所有配置文件中的同名配置

Sources: app/config.py, outfit_biz/src/outfit_biz/config.py

配置最佳实践

1. 安全性原则

Sources: .gitignore, .env.example

2. 开发环境配置


# 复制示例文件

cp .env.example .env



# 编辑 .env 文件,填写必要的配置

# 至少需要配置:

# - OPENAI_API_KEY(LLM 调用)

# - DATABASE_URL(数据库连接)

# - REDIS_URL(Redis 缓存,可选)

开发环境可以使用 DEV_TOKENDEV_USER_ID 简化测试流程。

Sources: README.md, docs/deployment.md

3. 生产环境配置

生产环境配置建议:

  1. 1. 使用环境变量而非 .env 文件:通过容器编排(Docker/K8s)注入环境变量
  2. 2. 启用内容安全审核:设置 ALIYUN_CONTENT_AUDIT_ENABLED=true
  3. 3. 配置监控:设置 HYPERDX_API_KEYOTEL_SERVICE_NAME
  4. 4. 启用 Redis 缓存:设置 LITELLM_REDIS_CACHE_ENABLED=true 降低 LLM 调用成本
  5. 5. 配置合理的并发参数:根据服务器资源调整 IMG_GEN_MAX_CONCURRENCY 等参数

Sources: ecosystem.config.js, docs/deployment.md

4. 配置验证

系统启动时会自动验证配置的有效性:

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.ymlmodel_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_KEYOPENAI_BASE_URL, LITELLM_REDIS_CACHE_ENABLED
数据库DATABASE_URLDATABASE_POOL_SIZE, DATABASE_MAX_OVERFLOW
RedisREDIS_URLLITELLM_REDIS_TTL
企业微信WECHAT_TOKEN, WECHAT_CORP_ID, WECHAT_SECRETWECHAT_MINIPROGRAM_APPID
内容安全ALIYUN_AK_ID, ALIYUN_AK_SECRETALIYUN_CONTENT_AUDIT_ENABLED
天气QWEATHER_API_HOST, QWEATHER_KEY_ID, QWEATHER_PROJECT_IDQWEATHER_PRIVATE_KEY_PATH
图片生成IMAGE_GEN_QUALITYIMAGE_GEN_PROVIDER, GEMINI_API_KEY
AdminADMIN_SECRET_KEYDEV_TOKEN, DEV_USER_ID
监控HYPERDX_API_KEY, OTEL_SERVICE_NAMEHYPERDX_API_ENDPOINT

下一步

配置完成后,建议继续阅读:

<a id="4-pei-zhi-wen-jian-jie-gou"></a>

---

配置文件结构

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_LEVELFastAPI 服务绑定参数
企业微信客服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_TTLLiteLLM 响应缓存开关
Mem0 记忆MEM0_ENABLED, MEM0_REALTIME_ANALYSIS_ENABLED, MEM0_COLLECTION_NAME长期语义记忆组件
Admin DashboardADMIN_SECRET_KEY管理后台 Bearer Token
监控HYPERDX_API_KEY, OTEL_SERVICE_NAMEOpenTelemetry 应用监控

环境变量的值在 app.config.AgentSettings 中通过 Pydantic BaseSettings 机制自动注入,覆盖 .envconfig.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_secondsLiteLLM 响应缓存、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
Embeddingembedding_batch_size_limitEmbedding 批处理上限

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 使用的模型角色别名(如 defaultflashvision),映射到 config.ymlmodel_map 节中定义的实际模型。

agent.team 控制主 Team(Leader)行为,关键参数包括:

参数默认值说明
add_history_to_contexttrue是否注入历史对话
num_history_runs10历史轮数
add_datetime_to_contexttrue时间感知(穿搭推理核心依赖)
timezone_identifier"Asia/Shanghai"时区
retries / exponential_backoff2 / trueLLM 调用容错
cache_sessiontrue降低 DB 读取延迟
store_events / store_member_responsestrue / trueAgentOS 追踪与 QC 审计
add_session_state_to_contexttrueLeader 感知 last_agent_id 路由状态
enable_session_summariesfalse多轮上下文压缩(未开启)
show_members_responsesfalse开发调试用
add_team_history_to_membersfalse是否将 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["提示词路径"]

字段类型说明
idstrTeam 唯一标识符
namestr显示名称
toolslist[str]全渠道共用的基础工具名列表
skillslist[str]可用技能 ID 列表,引用 data/skills/*/skill.toml
instructions.zh / .enstr中英文提示词文件路径(相对于 data/prompts/
[channels.<name>].toolslist[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 的参数:

字段类型说明
idstrAgent 标识符
namestr显示名称
modelstr模型角色别名(如 "vision"
role_keystr角色描述的 i18n 键名
add_history_to_contextbool是否注入历史
num_history_runsint历史轮数
tool_call_limitint单轮最大工具调用次数
retriesintLLM 重试次数
instructions.zh / .enstr提示词路径

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]categoryowner 等分类元数据
[instructions].zh / .en提示词文件路径

当前项目注册了四个技能:

技能 ID分类工具绑定
nuva-soulpersona(人格)无(纯提示词技能)
style-advisor-skilloutfit(穿搭建议)
buyer-wardrobe-skillbuyer-wardrobe(搜品/衣橱)18 个工具(搜索、链接、衣橱管理)
outfit-output-skilloutput(视觉输出)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_teamcanvas_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.*.jsonAgent 描述、工具返回消息、准入控制文案
agent_instructions.*.jsonAgent 指令文本
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. 1. outfit_biz/config.yml 提供基础配置
  2. 2. outfit_agent/config.yml 递归覆盖——同名标量直接替换,嵌套字典递归合并
  3. 3. 环境变量和 .env 通过 Pydantic BaseSettings 注入,优先级最高
  4. 4. AgentSettings 继承 BizSettings,添加 Agent 专属字段

AgentSettings 中的属性访问器(如 team_paramsmember_paramsleader_model)直接从 _merged_yml 读取对应的嵌套字典,透传给 Team.__init__()Agent.__init__()

Sources: app/config.py

灰度配置

项目支持用户级别的配置灰度覆盖,通过 outfit_biz.gray.GrayConfigService 实现。灰度覆盖可作用于:

灰度签名基于 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>

---

系统架构总览

本页是 Outfit Agent 系统的全局架构导览,面向首次接触代码库的开发者。文章从分层架构、请求生命周期、核心子系统三个维度,帮助你建立对整个系统的宏观认知,并指引你进入各专题的深度阅读。

技术栈概览

Outfit Agent 是一个基于 FastAPI + Agno Agent 框架 构建的智能穿搭顾问 REST API 服务。下表汇总了系统的核心技术选型与职责:

层次技术职责
Web 框架FastAPI + uvicornHTTP/WebSocket 路由,SSE 流式响应
Agent 框架Agno (agno.agent.Agent)LLM 编排、Tool 调用、会话持久化
AgentOSAgno AgentOSOpenTelemetry 追踪、Cron 调度、MCP Server
数据库PostgreSQL (asyncpg / psycopg2)业务数据、会话历史、AgentOS tracing
向量存储pgvectorMem0 记忆向量检索
缓存 / 队列Redis消息去重、会话队列、LiteLLM 缓存、实时 Run 状态
可观测性OpenTelemetry + HyperDX日志、链路追踪
模型调用LiteLLM → DashScope / OpenAILLM 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_toolAI 图片生成与虚拟试穿
图片搜索search_images_tool以图搜图
消息投递send_text_message, send_outfits, send_miniprogram_message跨渠道消息发送
Canvassearch_canvas_items, save_outfit_from画布编辑操作
用户update_user_profile, new_session用户画像与会话管理
对话状态get_dialog_state, update_dialog_stateSOP 流程状态读写
天气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_idlangsession_idavatar_pathchannel 等信息,所有业务工具通过 _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_stageSOP 当前阶段(如 collect_preferencesawait_outfit_confirmation
collected_slots已收集的用户偏好槽位
outfit_status穿搭工作流状态(runningawaiting_confirmationcompleted
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) 通过模块级状态字典确保消息的顺序处理:

渠道特定路由(如微信客服)只需提供 on_textprepare_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

工作流通过 OutfitWorkflowServiceasyncio.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_kefurouters/wechat.py企业微信客服回调(XML + 加密)
wechat_miniapprouters/miniapp/chat.py, ws.py小程序 REST + WebSocket
wechat_miniapp (Canvas)routers/miniapp/canvas_ws.py画布专用 WebSocket
apirouters/chat.pyREST 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 测试。各加载器(MainTeamLoaderSkillRegistry_load_tool_descriptions)均内置了灰度签名计算和缓存。

Sources: app/config.py

管理与可观测性

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              # 项目元数据与依赖

Sources: 项目目录结构, README.md

推荐阅读路径

基于以上架构全景,建议按以下顺序深入各专题:

  1. 1. 单主 Agent 架构 — 理解 Agent 单例构建、模型解析、运行时变体机制
  2. 2. 技能加载与管理 — 掌握 Skill 注册表、TOML 配置与灰度机制
  3. 3. Dialog State 设计 — 理解 SOP 流程如何驱动对话状态推进
  4. 4. 工具注册与工厂 — 深入工具包装、门控、描述管理
  5. 5. 消息入站处理流程 — 从用户消息到 Agent 调用的完整链路
  6. 6. 穿搭工作流服务 — 异步工作流编排与图片生成管线

<a id="6-dan-zhu-agent-jia-gou"></a>

---

单主 Agent 架构

本文档详细介绍了 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 的工具、技能和指令集。这种配置驱动的设计使得系统行为可以在不修改代码的情况下进行调整。

配置文件的主要结构包括:

例如,当前配置声明了 run_outfit_flowupdate_user_profilenew_session 等基础工具,以及 nuva-soulstyle-advisor-skillbuyer-wardrobe-skilloutfit-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 定义。它包含以下关键字段:

这些字段共同构成了对话流程的状态机,主 Agent 根据这些状态决定下一步行为。例如,当 sop_stageawait_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 实现,采用状态机模式管理文本和图片消息的处理顺序。

状态机的核心状态包括:

当用户发送文本消息时,系统首先检查是否有图片正在处理。如果有,文本消息会被加入待处理队列;如果没有,则检查是否有正在执行的文本任务。如果存在文本任务且新消息可以合并(防抖行为),则取消当前任务并将新消息合并到队列中;否则直接创建新任务执行。

图片消息的处理具有更高的优先级。一旦图片开始处理,所有后续消息(包括文本)都会被阻塞,直到图片处理完成。这种设计确保了图片处理流程的完整性,避免了图片处理过程中被其他消息干扰。

Sources: app/messaging/session_dispatcher.py

内存与上下文增强

系统集成了 Mem0 记忆模块,在每次 Agent 调用前检索相关记忆,为对话提供个性化上下文。记忆检索由 app/memory 模块的 search_user_memory() 函数实现,该函数基于语义相似度搜索用户的历史对话和记忆片段。

记忆上下文的注入遵循"读取前、写入后"的模式:

  1. 1. 读取前:在构建 Agent 输入前,先检索与当前消息相关的记忆
  2. 2. 构建输入:将记忆上下文、运行时上下文和用户消息组合成完整的输入
  3. 3. 写入后:在获得 Agent 回复后,异步地将对话记录添加到记忆中

这种设计确保了记忆操作不会阻塞主处理流程,同时为 Agent 提供了丰富的历史上下文。记忆上下文的长度受配置限制,避免过长的记忆片段影响 Agent 的处理效率。

Sources: app/agent.py, app/memory/__init__.py

快速响应与用户体验优化

为了提升用户体验,系统实现了快速响应机制,在 Agent 处理耗时操作时向用户发送即时反馈。该机制由 app/fast_response.py 模块实现,采用"工具调用响应优先"策略。

快速响应的工作流程:

  1. 1. 在 Agent 调用开始时,注册快速响应请求
  2. 2. 启动异步任务监听第一个工具调用响应
  3. 3. 如果在指定超时时间内收到工具调用响应,立即将该响应发送给用户
  4. 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_tracesagno_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. 1. 重新初始化工具注册表,加载最新的工具定义
  2. 2. 重新初始化技能注册表,扫描技能目录的变更
  3. 3. 重新加载团队配置文件
  4. 4. 清除 Agent 变体缓存
  5. 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>

---

技能加载与管理

本文档详细说明 Outfit Agent 系统中技能(Skill)的定义、加载、注册与运行时管理机制。技能是系统实现"单主 Agent + 动态 Skills"架构的核心抽象,它将业务能力模块化,使主 Agent 能够按需加载不同的业务场景能力。

技能系统概述

Outfit Agent 采用单主 Agent + 动态技能架构,技能是该架构的核心组成部分。主 Agent 负责意图识别、SOP 推进和对话状态管理,而具体的业务能力(如搜品、穿搭输出、人格表达)则封装在独立的技能模块中。这种设计实现了业务逻辑与核心调度的解耦,使得新增业务能力只需添加技能定义文件,无需修改主 Agent 代码。

技能系统的核心职责包括:

  1. 1. 业务能力封装:每个技能定义一组工具集合、指令提示词和激活条件
  2. 2. 工具门控:技能拥有的工具只有在技能被激活后才能被调用
  3. 3. 渠道适配:支持为不同渠道(如微信客服、小程序)配置不同的工具集
  4. 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

技能定义字段说明

字段类型必填说明
idstring技能唯一标识符
namestring技能显示名称,默认使用 id
descriptionstring技能描述,用于 on-demand 技能的提示词生成
enablebool是否启用,默认为 true
activationstring激活模式:on_demand(按需)或 always(常驻),默认为 on_demand
toolslist[string]全渠道共用的工具名称列表
allowed_toolslist[string]声明该技能推荐使用的工具边界(传递给 agno Skill)
channelstable渠道专属工具配置,键为渠道名,值为包含 tools 列表的表
compatibilitystring兼容性声明
licensestring许可证信息
metadatatable技能元数据
instructionstable提示词文件路径映射,键为语言代码,值为相对于 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. 1. 基础加载user_id=None):加载默认配置,结果缓存在 _skills 字典
  2. 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 提供了以下关键方法用于工具查询:

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 包含以下关键属性:

属性类型说明
nameslist[str]选中的技能 ID 列表
toolslist[Any]所有技能的工具对象列表
access_toolslist[Any]技能访问工具(get_skill_instructions 等)
prompt_snippetstron-demand 技能的系统提示片段
always_instruction_promptslist[str]always 技能的提示词路径列表
inline_instructionsstralways 技能的内联指令文本
definitionslist[RegisteredSkill]选中的技能定义列表

team_tools 属性返回 toolsaccess_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. 1. 从 SkillRegistry 查找指定技能
  2. 2. 调用 activate_skill(session_state, skill.id) 将技能写入对话状态
  3. 3. 加载并返回技能的指令提示词

activate_skill() 函数会更新对话状态中的 active_skillloaded_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

常驻白名单工具(不受门控限制):

Sources: app/dialog_state.py, app/dialog_state.py, app/tools/factory.py

渠道工具适配

技能支持为不同渠道配置专属工具,通过 skill.toml 中的 [channels.<channel_name>] 段落定义。运行时,tools_for_channel() 方法会合并全渠道工具和指定渠道的专属工具。

outfit-output-skill 为例:

渠道工具适配在两个层面生效:

  1. 1. 技能层面RegisteredSkill.is_tool_enabled_for_channel() 检查工具是否在当前渠道可用
  2. 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. 1. 计算灰度签名:遍历所有技能定义路径,构建 skill:<path> 格式的 scope 列表
  2. 2. 查询灰度规则:根据用户 ID 和 scope 列表查询匹配的灰度规则
  3. 3. 合并配置:使用 get_toml_overlay() 将灰度规则中的 TOML 片段合并到基础配置
  4. 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. 1. 工具合并:主 Agent 的基础工具(来自 team_cfg.tools)与技能工具(来自 SkillBundle.team_tools)合并
  2. 2. 指令注入always 技能的提示词自动注入系统指令;on_demand 技能的提示片段注入 additional_context
  3. 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-soulskills/nuva_soul.zh.txtskills/nuva_soul.en.txt
style-advisor-skillskills/style_advisor_skill.zh.txtskills/style_advisor_skill.en.txt
buyer-wardrobe-skillskills/buyer_wardrobe_skill.zh.txtskills/buyer_wardrobe_skill.en.txt
outfit-output-skillskills/outfit_output_skill.zh.txtskills/outfit_output_skill.en.txt

提示词内容定义了技能的职责边界、执行规则、输出格式等。Agent 在加载技能指令后,必须严格按照提示词中的约束执行。

Sources: app/skills/loader.py, data/prompts/skills/

技能资源文件

技能可以携带参考资料(references)和脚本(scripts)两类资源文件,分别存放在技能源目录的 references/scripts/ 子目录中。资源文件在技能加载时自动发现,并通过运行时访问工具提供给 Agent。

Sources: app/skills/loader.py, app/skills/access_tools.py

下一步

<a id="8-ji-neng-gong-ju-men-kong-ji-zhi"></a>

---

技能工具门控机制

本文档详细阐述 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_flowupdate_user_profilenew_sessionsend_text_messagesend_miniprogram_messageget_skill_instructionsget_skill_referenceget_skill_scriptget_dialog_stateupdate_dialog_state。这些工具是系统运转的基础设施,不应被门控阻断。

Sources: dialog_state.py, dialog_state.py

技能激活流程

技能处于被动激活状态——Agent 需要主动调用访问工具(Access Tools)来激活技能。access_tools.py 提供了三个访问工具:

  1. 1. get_skill_instructions(skill_name):读取技能的提示词指令,并将该技能标记为当前会话的 active_skill
  2. 2. get_skill_reference(skill_name, reference_name):读取技能附属的参考文档,同时激活技能
  3. 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>] 段为不同渠道声明专属工具。运行时,MainTeamConfigallowed_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_skilloutfit-output-skill 且当前渠道为 wechat_kefu 时,调用 edit_outfit 会被技能级渠道门控拦截。

RegisteredSkill 通过以下方法管理渠道工具:

Sources: outfit-output-skill/skill.toml, loader.py

渠道门控执行流程

_tool_available_for_current_channel 函数按以下顺序检查:

  1. 1. 从当前会话的 session_state 中解析出 current_channel
  2. 2. 团队级检查:查询团队配置的 allowed_channels_for_tool,若工具被限制且当前渠道不在允许列表中,直接返回错误
  3. 3. 技能级检查:如果存在 active_skill,查询该技能的 allowed_channels_for_tool,若工具被限制且当前渠道不在允许列表中,返回错误
  4. 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. 1. 调用 _wrap_tool_entrypoint 包装入口函数,注入渠道门控和技能归属门控逻辑
  2. 2. 通过 Function.from_callable 创建 agno Function 对象
  3. 3. 从 tools/descriptions.toml 加载工具描述
  4. 4. 处理 __tool_stop_after_tool_call____tool_stop_after_tool_call_if____tool_show_result__ 等装饰属性

包装后的工具在每次调用时会:

  1. 1. 绑定 Agno 上下文(用于链路追踪)
  2. 2. 执行渠道门控检查
  3. 3. 执行技能归属门控检查
  4. 4. 两层检查均通过后,调用原始入口函数
  5. 5. 记录追踪信息

Sources: factory.py, factory.py

工具注入时机

_build_agent 函数中,工具通过两个来源合并注入到主 Agent:

  1. 1. 团队工具get_main_agent_tools(team_cfg) 根据团队配置解析基础工具列表
  2. 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

灰度配置支持

门控机制完整支持灰度配置覆盖。SkillRegistryMainTeamLoader 都通过 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

参考与导航

<a id="9-dialog-state-she-ji"></a>

---

Dialog State 设计

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 的完整骨架。每个字段承担明确的职责,不存在语义重叠。以下是字段分类与类型说明:

字段类型职责分类说明
versionint元数据当前固定为 1,预留 schema 演进能力
active_skill`str \None`技能上下文当前激活的技能 ID,影响工具门控
loaded_skillslist[str]技能上下文已加载的技能 ID 列表(去重、保序)
active_scene`str \None`业务场景当前业务场景标识(如 outfit_recommendation
stage`str \None`业务场景泛化业务阶段标记
sop_stage`str \None`SOP 推进SOP 流水线阶段,详见下方 SOP 阶段表
collection_plan`dict \None`信息收集信息收集策略(task_kindrequired_slotsmissing_slotsoptional_slots
collected_slotsdict信息收集已收集的槽值(键值对)
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`用户确认待确认事项(含 actionsummary
updated_atint元数据最后更新的 Unix 时间戳

Sources: app/dialog_state.py

核心操作函数

状态读取与初始化

系统提供两个读取函数,分别服务于可写场景只读场景,选择不同的合并策略:

两个函数都会调用 _normalize_skill_list()loaded_skills 做去重保序处理,并确保 collected_slots 始终为 dict 类型。

Sources: app/dialog_state.py

状态更新

update_dialog_state_fields() 是状态写入的唯一核心函数,所有更新路径最终都汇聚于此。它接受丰富的 keyword-only 参数,支持增量更新条件清除两种模式:

值得注意的是,该函数内嵌了一条状态归一化逻辑:当 sop_stageawait_outfit_confirmationpending_confirmation.actionrun_outfit_flow 时,如果 outfit_status 为空或仍为 running,系统会自动将其归一化为 awaiting_confirmation,同时清空 outfit_run_idoutfit_result_summary。这条规则确保了"等待确认"状态的一致性。

Sources: app/dialog_state.py

技能激活

activate_skill() 是技能加载的专用入口,执行以下原子操作:

  1. 1. 调用 ensure_dialog_state() 确保状态已初始化
  2. 2. 将 skill_name 追加到 loaded_skills(若不存在)
  3. 3. 将 active_skill 设为 skill_name
  4. 4. 可选地同时写入 sop_stageactive_scene

Sources: app/dialog_state.py

工具门控机制

Dialog State 驱动了系统的技能级工具门控,这是防止 Agent 在错误的 SOP 阶段调用不相关工具的核心安全机制。

门控规则

is_tool_allowed(session_state, tool_name, owning_skills) 的判定逻辑:

  1. 1. 若 tool_nameALWAYS_ALLOWED_TOOLS 集合中 → 始终放行
  2. 2. 若 owning_skills 为空(工具不属于任何技能)→ 始终放行
  3. 3. 否则,检查 active_skill 是否在 owning_skills 列表中 → 匹配则放行,否则拒绝

ALWAYS_ALLOWED_TOOLS 包含 10 个系统级工具:run_outfit_flowupdate_user_profilenew_sessionsend_text_messagesend_miniprogram_messageget_skill_instructionsget_skill_referenceget_skill_scriptget_dialog_stateupdate_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 错误,包含 errortool_namerequired_skillmessage 字段,供 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 阶段断点处强制等待用户的明确意图。其结构包含两个固定键:

该机制在以下场景中被激活:

  1. 1. 穿搭任务发车确认:当 outfit_task 写入后,pending_confirmation.action 设为 run_outfit_flowsop_stage 推进到 await_outfit_confirmation。只有当用户明确表达"开始"等意图时,Agent 才被允许执行 run_outfit_flow
  2. 2. 搜品结果确认buyer-wardrobe-skill 完成搜品后,通过 pending_confirmation 等待用户对结果的认可。
  3. 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_skillloaded_skillsactive_scenestagesop_stagecollection_plancollected_slotsidea_summaryoutfit_taskoutfit_statusoutfit_run_idoutfit_result_summarypending_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. 1. 通过 aread_or_create_session() 从 Agno Session Store 加载会话
  2. 2. 通过 load_session_state() 恢复 session_state
  3. 3. 调用 update_dialog_state_fields() 应用增量更新
  4. 4. 将更新后的状态写回 session_data["session_state"]
  5. 5. 通过 asave_session() 持久化

该函数接受 session_iduser_idupdate_kwargs(直接透传给 update_dialog_state_fields()),确保了 Agent 运行时和异步回调对 Dialog State 的操作使用同一套合并逻辑。

Sources: app/services/dialog_state_store.py

可观测性

Dialog State 的关键字段被系统性地暴露到两个可观测性通道:

  1. 1. 实时运行快照app/live_runs.py):_dialog_state_summary() 提取 11 个核心字段的摘要,用于 Dashboard 实时展示会话状态。
  2. 2. Trace 属性app/tools/factory.py):_build_trace_attributes()active_skillactive_scenesop_stageoutfit_statusoutfit_run_idpending_confirmationloaded_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_stateactive_scenestagesop_stage_json(JSON 字符串参数)、outfit_statusoutfit_run_idclear_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 阶段推进

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_flowawait_outfit_confirmationoutfit_running 的唯一合法入口。该工具内置了严格的前置检查——只有当 sop_stage == "await_outfit_confirmation"pending_confirmation.action == "run_outfit_flow" 时才放行。执行成功后,通过 resolve_outfit_workflow_sop_stage() 将工作流状态映射为最终 SOP 阶段:


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. 1. wechat_followup_intent.py:当 followup 意图仲裁器判定为 interrupt 动作且检测到穿搭工作流正在运行时,通过 persist_dialog_state_update()sop_stage 回退到 await_outfit_confirmation,同时清除 outfit_run_id 并写入中断相关的 pending_confirmation
  2. 2. outfit_workflow_service.py:工作流内部捕获 asyncio.CancelledError 时,同样回退到 await_outfit_confirmation。服务启动时的 recover_interrupted_workflows() 方法还会扫描数据库中所有 outfit_status == "running" 的会话,根据工作流实际运行状态决定恢复为 s3_visual_deliveredawait_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_stageawait_outfit_confirmationpending_confirmation.actionrun_outfit_flow 时,只有当用户明确表达"开始"等意图后,Agent 才被允许执行 run_outfit_flow

系统在 update_dialog_state_fields() 中内嵌了一条状态归一化逻辑:当上述两个条件同时满足时,如果 outfit_status 为空或仍为 running,系统会自动将其归一化为 awaiting_confirmation,同时清空 outfit_run_idoutfit_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_stageoutfit_status含义
await_outfit_confirmationawaiting_confirmation任务清单就绪,等待用户发车
outfit_runningrunning工作流正在后台执行
s3_visual_deliveredcompleted / partial_failed视觉方案已交付(全部/部分成功)
outfit_failedfailed工作流执行失败
await_outfit_confirmation(中断恢复)interrupted被用户新消息中断,回退到确认态

工作流本身由 OutfitGenerationWorkflow 定义,包含五个顺序 Step:prepare_contextideate_outfitsselect_itemsgenerate_images(并发)→ dispatch_resultsdispatch_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>

---

用户确认机制

用户确认机制是 NUVA Fashion OS 中确保穿搭生成工作流可控执行的核心安全机制。该机制通过对话状态管理(Dialog State)和 SOP 阶段控制,在关键操作执行前强制插入用户确认环节,防止未经用户授权的后台任务执行,同时支持工作流中断后的状态恢复。

机制概述与设计原则

用户确认机制的核心设计遵循"显式授权"原则:任何涉及资源密集型操作(如穿搭图生成、单品搜索、试穿渲染)的任务,都必须在用户明确确认后才能执行。这种设计既避免了系统资源的浪费,也确保了用户对生成结果的预期管理。

该机制通过三个核心状态字段实现:

当系统完成需求收集并准备好执行生成任务时,会进入"等待确认"状态;只有用户明确表达"开始生成"等意图后,系统才会执行实际的生成操作。

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 字段是一个字典,包含两个关键属性:

状态流转遵循严格的时序逻辑:

  1. 1. 需求收集阶段:系统通过 collection_plan 收集用户需求,sop_stagecollecting_requirements
  2. 2. 任务准备阶段:需求收集完成后,系统写入 outfit_tasksop_stage 变为 await_outfit_confirmation
  3. 3. 用户确认阶段pending_confirmation.action 设置为 run_outfit_flow,等待用户确认
  4. 4. 执行阶段:用户确认后,sop_stage 变为 outfit_runningoutfit_status 变为 running
  5. 5. 完成阶段:工作流完成后,sop_stage 变为 s3_visual_delivered

Sources: dialog_state.py, dialog_state.py

工具层实现与门控逻辑

用户确认机制在工具层通过 outfit_workflow_tools.pyoutfit_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. 1. 状态验证:检查 sop_stage 是否为 await_outfit_confirmation
  2. 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. 1. 检测中断条件:通过 _should_reset_outfit_confirmation_state 函数检测是否需要中断
  2. 2. 更新对话状态:将 sop_stage 重置为 await_outfit_confirmation
  3. 3. 设置待确认事项:创建新的 pending_confirmation,提示用户是否恢复中断的工作流
  4. 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_confirmationpending_confirmation.action == run_outfit_flow 时才能执行;否则会返回错误。

update_dialog_state 工具的描述也明确了如何设置待确认事项:

若需要用户确认,可传入 pending_confirmation_actionpending_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. 1. 状态验证测试:验证 run_outfit_flow 在未满足确认条件时返回错误
  2. 2. 正常执行测试:验证工作流在正确确认后能够正常执行
  3. 3. 状态更新测试:验证工作流完成后对话状态正确更新
  4. 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 {

        [*] --> 等待用户确认

        等待用户确认 --> 确认开始: 用户说"开始生成"

        等待用户确认 --> 补充信息: 用户补充需求

    }

相关页面

<a id="12-gong-ju-zhu-ce-yu-gong-han"></a>

---

工具注册与工厂

本文档详细阐述 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. 1. 名称解析:优先使用显式 name 参数,否则从函数名推导
  2. 2. 入口包装:通过 _wrap_tool_entrypoint() 为函数添加安全上下文注入和追踪能力
  3. 3. 描述加载:从 data/tools/descriptions.toml 加载工具描述,支持用户级灰度覆盖
  4. 4. 行为配置:根据函数装饰器属性设置 stop_after_tool_callshow_result 等行为
  5. 5. 钩子安装:为动态停止条件安装 pre_hookpost_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()1data/tools/descriptions.toml 加载全局描述
用户层_load_tool_descriptions(user_id)128基础层 + 用户级灰度覆盖

Sources: app/tools/factory.py, data/tools/descriptions.toml

安全包装机制

RunContext 与 ToolContext

工具包装的核心安全目标是防止 LLM 越权访问用户数据。通过 RunContext 注入会话级上下文,确保 LLM 无法直接操作 user_idlang 等敏感参数。


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. 1. 上下文绑定:通过 bind_agno_context() 绑定 session_idrun_iduser_id 到追踪上下文
  2. 2. 调用追踪:通过 trace_tool_call() 记录工具调用的输入、输出和属性
  3. 3. 渠道门控:检查工具是否在当前渠道(channel)可用
  4. 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_toolssearch_canvas_items, save_outfit_from画布相关工具
item_toolssearch_items, get_item_detail单品管理工具
outfit_toolsgenerate_outfit, edit_outfit穿搭管理工具
image_gen_toolsgenerate_image, generate_tryon_tool图片生成工具
shopping_toolsget_item_links_tool, get_cheapest_link_tool购物链接工具
user_toolsupdate_user_profile, new_session用户管理工具
messaging_toolssend_text_message, send_outfits消息发送工具
weatherquery_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

常驻允许工具

以下工具不受门控限制,始终可用:

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. 1. pre_hook:每次调用前重置 stop_after_tool_call = False
  2. 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. 1. 创建工具函数:在 app/tools/ 下对应模块中创建业务函数
  2. 2. 添加包装函数:在函数签名中添加 run_context: RunContext 参数,通过 _get_ctx() 获取上下文
  3. 3. 注册到 TOOLS 列表:在模块末尾的 TOOLS 列表中添加函数引用
  4. 4. 更新注册表:在 app/tools/__init__.py_build_tool_registry() 中添加映射
  5. 5. 添加工具描述:在 data/tools/descriptions.toml 中添加工具描述
  6. 6. 配置技能声明:在相应技能的 skill.toml 中声明工具

安全注意事项

Sources: app/tools/context.py, app/tools/item_tools.py

相关页面

<a id="13-gong-ju-miao-shu-guan-li"></a>

---

工具描述管理

工具描述管理是 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. 1. 场景说明:明确描述何时使用该工具,避免误用
  2. 2. 参数说明:详细说明必选和可选参数,包括格式要求
  3. 3. 行为边界:明确工具的使用限制和替代方案
  4. 4. 返回格式:说明返回的数据结构和字段含义
  5. 5. 注意事项:包含重要的警告和最佳实践

Sources: data/tools/descriptions.toml

工具描述加载机制

系统采用分层缓存策略加载工具描述,确保性能的同时支持灰色发布。

加载流程

  1. 1. 基础描述加载:从 data/tools/descriptions.toml 文件读取基础描述
  2. 2. 用户覆盖加载:检查是否有针对当前用户的灰色发布描述
  3. 3. 描述合并:将用户特定描述与基础描述合并
  4. 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()},

    }

缓存策略

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. 1. 描述覆盖:用户特定描述文件结构与基础文件相同,但只包含需要覆盖的工具
  2. 2. 合并策略:用户特定描述完全覆盖基础描述中的同名工具
  3. 3. 缓存机制:每个用户的描述独立缓存,避免相互影响
  4. 4. 性能优化:使用 LRU 缓存,最多缓存 128 个用户的描述

使用场景

管理接口: 灰色发布通过 Admin API 管理,具体接口在 app/routers/admin/gray.py 中定义:

Sources: app/routers/admin/gray.py, tests/test_tool_description_gray.py

工具描述管理接口

系统提供完整的 Admin API 用于管理工具描述文件。

核心接口

接口方法功能说明
/admin/tool-descriptionsGET获取元信息返回文件大小、修改时间、工具列表等
/admin/tool-descriptions/contentGET获取文件内容返回完整的 TOML 文件内容
/admin/tool-descriptions/contentPUT更新文件内容更新工具描述文件并清除缓存
/admin/tool-descriptions/reloadPOST热重载重新加载工具描述到运行时
/admin/tool-descriptions/historyGET获取历史版本获取工具描述的历史版本列表
/admin/tool-descriptions/history/{version}GET获取版本内容获取指定历史版本的内容
/admin/tool-descriptions/rollbackPOST回滚版本将工具描述回滚到指定版本

热重载机制


@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))

版本管理

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. 1. 技能激活时:当用户激活特定技能时,系统加载该技能的工具描述
  2. 2. 工具调用时:在工具调用前,系统检查工具是否在当前技能上下文中允许
  3. 3. 描述覆盖:技能可以覆盖基础工具描述,提供更具体的上下文指导

工具描述与技能的协同

Sources: app/dialog_state.py, app/skills/loader.py

最佳实践

工具描述编写指南

  1. 1. 场景明确:清晰描述工具的使用场景,避免误用
  2. 2. 参数详细:详细说明必选和可选参数,包括格式要求和取值范围
  3. 3. 行为边界:明确工具的使用限制和替代方案
  4. 4. 返回格式:说明返回的数据结构和字段含义
  5. 5. 注意事项:包含重要的警告和最佳实践

灰色发布策略

  1. 1. 小范围测试:先对小范围用户测试新工具描述
  2. 2. 效果监控:监控工具描述变更对 LLM 行为的影响
  3. 3. 快速回滚:准备快速回滚机制,应对问题描述
  4. 4. 逐步发布:通过灰色发布逐步扩大新描述的适用范围

性能优化

  1. 1. 缓存利用:充分利用 LRU 缓存,避免重复文件读取
  2. 2. 延迟加载:只在需要时加载用户特定描述
  3. 3. 缓存清理:及时清理无效缓存,确保数据一致性
  4. 4. 监控指标:监控工具描述加载的性能指标

安全考虑

  1. 1. 输入验证:验证工具描述内容的格式和结构
  2. 2. 权限控制:限制工具描述的修改权限
  3. 3. 审计日志:记录工具描述的修改和访问日志
  4. 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>

---

工具调用链路追踪

在复杂的 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)

追踪属性

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: 更新监控面板

核心功能

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. 1. 上下文提取:从 RunContext 提取会话、运行和用户信息
  2. 2. 追踪绑定:绑定 OpenTelemetry 追踪上下文
  3. 3. 门控检查:执行工具访问权限和渠道限制检查
  4. 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 [],

    }

属性分类

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

日志内容

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/sessionsGET分页列出会话(支持多维度过滤)
/traces/sessions/{session_id}GET获取会话详情(包含所有运行摘要)
/traces/runs/{run_id}GET获取单次运行的完整跨度树
/traces/live-runsGET获取当前进程内正在运行的运行列表
/traces/live-runs/{run_id}GET获取单个实时运行的详细信息
/traces/runs/{run_id}/retry-image-generationPOST重新入队生图任务

查询参数示例


# 按用户 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 提供可视化的追踪面板,支持以下功能:

会话列表视图

运行详情视图

实时运行监控

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. 1. 使用工具包装:所有工具必须通过 build_tool 函数注册
  2. 2. 提供清晰描述:在 tools/descriptions.toml 中为每个工具提供描述
  3. 3. 处理错误:工具内部错误应被捕获并记录到追踪系统
  4. 4. 设置超时:为长时间运行的工具设置合理的超时时间

工具描述示例


# tools/descriptions.toml

[tools]

search_items = "根据关键词搜索商品,支持多维度筛选"

generate_outfit = "生成穿搭方案,包含图片和描述"

send_text_message = "发送文本消息给用户"

Sources: data/tools/descriptions.toml

2. 追踪属性使用

推荐使用的追踪属性

避免的属性

3. 性能优化建议

追踪性能优化

  1. 1. 采样率控制:在生产环境设置合理的采样率
  2. 2. 批量处理:使用批量处理器减少数据库写入
  3. 3. 异步导出:确保追踪导出不阻塞主业务流程
  4. 4. 索引优化:为常用查询字段创建数据库索引

内存使用优化

  1. 1. 及时清理:确保追踪上下文及时清理
  2. 2. 限制大小:限制单个追踪记录的大小
  3. 3. 压缩存储:对大型追踪数据使用压缩存储

故障排查

1. 常见问题排查

追踪数据丢失

  1. 1. 检查环境变量配置:HYPERDX_API_KEYOTEL_SERVICE_NAME
  2. 2. 检查数据库连接:确保 PostgreSQL 连接正常
  3. 3. 检查 Redis 连接:确保 Redis 连接正常
  4. 4. 查看应用日志:检查追踪初始化错误

实时监控延迟

  1. 1. 检查 Redis 性能:确保 Redis 响应时间正常
  2. 2. 检查心跳间隔:调整 live_run_heartbeat_interval_seconds
  3. 3. 检查 TTL 设置:调整 live_run_redis_ttl_seconds

工具调用未追踪

  1. 1. 检查工具注册:确保工具通过 build_tool 注册
  2. 2. 检查包装层:确保 _wrap_tool_entrypoint 正常工作
  3. 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. 1. 完整的追踪链路:从工具调用到 LLM 请求的完整追踪
  2. 2. 实时监控能力:基于 Redis 的实时运行状态监控
  3. 3. 丰富的查询接口:Admin API 提供多维度的追踪查询
  4. 4. 可视化支持:Dashboard 提供直观的追踪面板
  5. 5. 灵活的扩展性:支持自定义追踪属性和第三方集成

通过合理使用追踪系统,开发者可以快速定位性能瓶颈、调试工具调用问题,并优化 Agent 系统的整体性能。

下一步

了解工具调用链路追踪后,建议继续阅读:

<a id="15-xiao-xi-ru-zhan-chu-li-liu-cheng"></a>

---

消息入站处理流程

本文档系统梳理从用户发送消息到 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) 解耦:渠道层只需实现 TextHandlerImagePreprocessorImageErrorHandlerFollowupArbiter 四个接口,即可接入统一调度。

Sources: session_dispatcher.py, \_\_init\_\_.py

阶段一:渠道接收与解密

每个渠道入口对接各自的传输协议,但最终都标准化为 (content, image_ref) 两种消息类型进入调度层。

微信客服渠道

微信客服消息通过同步拉取模式获取。系统轮询 sync_msg API 拉取新消息批次,对每条消息逐一处理。

接收流程POST /wechat/callback → XML 解密 → _process_kf_eventasync_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_textdispatch_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_memoryrun_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_idmedia_id微信客服图片,需通过 media_fetcher 下载
stored_image_idimage_id(正整数)数据库已存储图片,直接查询
remote_urlurl外部 URL 图片,直接下载

prepare_image_message 函数根据 ImageInputRef.kind 分发到不同处理路径:已存储图片直接构建 ImagePipelineResult;其他来源先获取字节流再走图像管道。

Sources: inbound.py

阶段三:快捷方式匹配

在消息进入 Agent 调用之前,系统首先尝试匹配入站快捷方式inbound_shortcuts)。快捷方式是一组预定义规则,用于对特定关键词返回固定回复,避免触发完整的 Agent 调用链路。

匹配逻辑在两个层级执行:

  1. 1. 调度层 — 渠道路由中调用 resolve_inbound_shortcut,匹配后直接发送快捷回复(如微信客服的 wechat_demo_miniprogram 类型)
  2. 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.typetext(纯文本)、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_taskssession_idasyncio.Task当前正在运行的文本 Agent 任务
_text_task_contentssession_idstr传给当前任务的内容(用于合并)
_image_processingsession_idasyncio.Event图像处理进行中标记
_queue_drainingsession_id(集合)防止 drain_queue 重入

所有状态仅从 asyncio 事件循环单线程修改,无需加锁。

文本消息调度(dispatch_text

当新文本消息到达时,调度器根据当前状态做出三种决策:

  1. 1. 有图像在飞或队列非空 → 消息入队,等待后续 drain
  2. 2. 有旧文本任务运行 → 调用 FollowupArbiter 仲裁,支持四种策略:
  1. 3. 无任务运行 → 直接调度新任务

新任务创建后会进入 0.3 秒防抖窗口(debounce),在此期间如果被更新的任务取消,就不会调用 Agent。防抖通过 dispatch_guard 中的 ContextVar 实现任务身份验证,确保过期任务不会误执行。

Sources: session_dispatcher.py, dispatch_guard.py

图像消息调度(dispatch_image

图像消息具有最高优先级

  1. 1. 如果有正在运行的文本任务 → 取消它,保留其内容加入图像批次
  2. 2. 标记图像处理开始(mark_image_in_flight
  3. 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 包含三个关键字段:

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 跟踪每条消息的回复状态,防止重复回复。关键机制:

Sources: agent_service.py, reply_state.py

阶段七:显示过滤与回复发送

Agent 生成的回复在发送给用户前会经过显示过滤器display_filters.py)处理,移除内部推理标记(如 </nuva_thought> 之前的内容)。这确保用户只看到最终结论,而非 Agent 的中间思考过程。

过滤规则为硬编码的元组:

渠道层在 send_reply 回调中应用过滤后再发送。以微信客服为例,过滤后的文本还会经过 rewrite_wechat_mp_links_for_chat 将小程序链接转为微信可解析格式,再通过 build_wechat_reply_delivery 决定是以纯文本还是菜单消息形式发送。

Sources: display_filters.py, wechat.py

后续消息仲裁(Followup Arbiter)

当用户在 Agent 处理过程中连续发送消息时,FollowupArbiter 决定如何处理新消息。微信渠道实现了自定义仲裁器 arbitrate_wechat_followupwechat_followup_intent.py),它结合 Agno 运行时快照来判断:

仲裁结果为 FollowupDecision,包含 actioninterrupt/parallel/handled/fresh)、可选的 replacement_contentmessage_records

Sources: wechat_followup_intent.py, session_dispatcher.py

关键设计模式总结

模式应用位置解决的问题
Protocol 解耦TextHandlerImagePreprocessor渠道层与调度层解耦,新渠道只需实现协议接口
防抖(Debounce)_schedule_text_task 中 0.3s 延迟用户快速连续输入时合并消息,减少 Agent 调用
任务身份验证dispatch_guard.py 中的 ContextVar确保过期异步任务不会误执行
队列 draindrain_queue 循环图像处理完成后自动恢复被阻塞的文本消息
幂等去重Redis msgid 缓存微信消息重传不会重复处理
双层快捷方式路由层 + Agent 服务层简单请求快速响应,避免完整 Agent 链路开销

相关文档

<a id="16-wei-xin-ke-fu-yu-xiao-cheng-xu-ji-cheng"></a>

---

微信客服与小程序集成

本页文档详解系统与微信生态的两条集成通道——企业微信客服(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 APIWebSocket 推送 + HTTP 回复
认证方式access_token(Corp ID + Secret)用户 JWT Token
会话标识external_userid + open_kfiduser_id(数据库主键)
渠道标识RegisterChannel.WECHAT_KEFURegisterChannel.WECHAT_MINIAPP
消息类型支持text / image / voice / event / msgmenutext / image(multipart)
并发控制Redis 去重 + 消息配额会话调度器(通用)

Sources: config.py, main.py

企业微信客服通道

企业微信客服通道通过 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 哈希,将 tokentimestampnonceencrypt 排序后拼接比较。

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_msgAgent 回复、错误提示
图片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 负责解析 选项文本 语法,生成包含 clickviewminiprogram 等菜单项的消息体。

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. 1. 注册图片处理中的状态(mark_image_in_flight
  2. 2. 下载微信图片素材(kf/get_media
  3. 3. 运行图片处理流水线
  4. 4. 将图片描述注入 Agent 上下文
  5. 5. 完成后清除处理状态

Sources: routers/wechat.py, routers/wechat.py

微信小程序通道

小程序通道提供了比客服通道更丰富的交互能力。它包含 HTTP REST API 和 WebSocket 两种接入方式,支持流式对话和实时事件推送。

HTTP API 接口

小程序 HTTP API 以 /api/wechat/ 为前缀,需要 JWT Token 认证(通过 LoginRequired 依赖注入)。主要端点包括:

端点方法功能
/api/wechat/chatPOST发送聊天消息(支持 text + image 混合)
/api/wechat/chat/upload-imagePOST上传聊天图片(最大 50MB)
/api/wechat/wsWebSocket实时聊天与事件推送
/api/wechat/user/loginPOST小程序登录(code 换 token)
/api/wechat/user/profileGET/PUT用户资料读写

聊天端点 POST /api/wechat/chat 接受 MiniappChatRequest,支持两种消息格式:

请求处理流程:构建入站消息部件 → 内容审核 → 访问控制 → 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. 1. 流式对话:消息通过 WebSocket 发送,Agent 回复逐 token 推送
  2. 2. 穿搭结果推送:后台生成完成后,通过 send_miniapp_event 实时推送穿搭卡片
  3. 3. 状态同步:支持 outfit 轮询模式——客户端发送 outfit 请求后,服务端以 0.8 秒间隔轮询数据库状态,完成后立即推送

连接管理通过 _user_connections 字典维护(user_idset[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 获取 openidsession_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_kfidexternal_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渠道专属穿搭摘要模板

当前支持的渠道目录:

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_historypages/history-images/history-images.html生成历史
outfit_detailpages/chat/chat.html穿搭详情(重写为当前聊天)
my_closetpages/closet/closet.html我的衣橱
edit_profilepages/info-collect/info-collect.html编辑资料

在客服通道中,这些链接被转换为 Markdown 菜单项;在小程序通道中,被转换为纯文本或 WebSocket 选项。

Sources: wechat/miniprogram_routes.py, wechat/miniprogram_routes.py

内容安全审核

两条通道的消息都经过阿里云内容安全审核,但策略有所不同:

审核维度客服通道小程序通道
文本审核启用启用
图片审核启用启用
审核渠道过滤默认不审核(配置可开启)默认审核
置信度阈值95%95%

config.ymlaliyun_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

调试与运维

客服通道关键日志标签

小程序通道关键日志标签

Redis 键清单(客服通道):

键模式TTL用途
wechat:kefu:access_token7000saccess_token 缓存
wechat:kefu:cursor持久消息同步游标
wechat:kefu:msg:{msgid}24h消息去重
wechat:kefu:known_user:{extid}7d欢迎语去重
wechat:kefu:quota:{extid}48h消息配额计数
wechat:kefu:thumb_media_id2.5d缩略图素材缓存

<a id="17-hui-hua-fen-fa-yu-hui-fu-zhuang-tai"></a>

---

会话分发与回复状态

当用户在短时间内连续发送多条消息(文本+图片、多条文本等),系统必须保证 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_tasksdict[str, asyncio.Task]当前正在运行的文本 Agent 任务
_text_task_contentsdict[str, str]正在运行任务的原始内容(用于后续合并)
_image_processingdict[str, asyncio.Event]图片处理中的阻塞信号
_queue_drainingset[str]正在排空队列的 session(重入保护)

当一个 Agent 任务完成后,调度器在 finally 块中自动调用 drain_queue 来处理队列中下一条消息,形成链式排空

Sources: session_dispatcher.py

消息分发入口函数

调度器对外暴露三个入口函数,渠道路由(如微信客服、小程序)通过它们驱动状态机,从不直接操作内部字典

dispatch_text

文本消息的分发入口。其核心逻辑分为三条路径:

  1. 1. 阻塞路径:若图片正在处理或队列非空,消息直接入队等待
  2. 2. 仲裁路径:若有正在运行的文本任务,通过 FollowupArbiter 决策后行动
  3. 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

队列排空函数是整个调度器的"心脏"。它在每次任务完成后被自动调用,从数据库中弹出待处理消息并按类型分发:

重入保护机制通过 _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_textTextHandler执行 Agent 调用并发送回复
prepare_imageImagePreprocessor将渠道图片引用解析为 ImagePipelineResult
on_image_errorImageErrorHandler图片预处理失败时发送错误提示
arbiterFollowupArbiter对并发消息进行意图分类决策

入站消息经过访问控制内容审核快捷方式匹配等前置处理后,才进入 dispatch_text / dispatch_image。消息的 reply_state_key(如微信 msgid)在入口处即被注册到 Redis,确保后续的回复状态追踪覆盖完整生命周期。

Sources: wechat.py, wechat.py

图片批处理流程

当图片到达时,调度器不会立即调用 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 记忆管理

本页介绍 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_enabledinsight_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. 1. 向量存储(pgvector):使用项目现有的 PostgreSQL 数据库,collection 名称默认为 outfit_mem0_memories,embedding 维度与 text_embedding 模型配置一致(默认 1024)。
  2. 2. LLM 提取器:使用 flash 模型(默认 temperature=0.1)驱动记忆事实提取。
  3. 3. Embedding 模型:使用 text_embedding 模型生成语义向量,通过 OpenAI 兼容接口调用。

Sources: app/memory/client.py, app/main.py

配置参数

所有记忆相关配置通过环境变量或 config.yml 控制,由 app/config.pySettings 类统一管理:

配置项环境变量默认值说明
mem0_enabledMEM0_ENABLEDtrue是否启用 Mem0 记忆系统
mem0_realtime_analysis_enabledMEM0_REALTIME_ANALYSIS_ENABLEDfalse是否对每轮对话实时提取记忆
mem0_collection_nameMEM0_COLLECTION_NAMEoutfit_mem0_memoriespgvector 集合名称
mem0_search_limitMEM0_SEARCH_LIMIT10单次语义检索最大返回条数
mem0_history_db_pathMEM0_HISTORY_DB_PATHdata/mem0_history.dbMem0 内部记忆变更历史 SQLite 路径

离线洞察分析的额外配置:

配置项默认值说明
insight_outfit_delta_threshold20触发穿搭洞察的新增穿搭数阈值
insight_item_delta_threshold20触发衣橱洞察的新增单品数阈值
insight_chat_interval_days30聊天洞察的间隔天数
insight_merge_history_days730仲裁时穿搭/聊天历史窗口(天)
insight_scheduler_cron0 0 *每日调度 cron 表达式
insight_scheduler_timezoneAsia/Shanghai调度时区
insight_batch_size200每批扫描用户数
insight_user_concurrency5并发处理用户数

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.pyrun_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. 1. 穿搭增量:距上次穿搭洞察后,新增穿搭记录数 >= insight_outfit_delta_threshold(默认 20)
  2. 2. 衣橱增量:距上次衣橱洞察后,新增 source in (wardrobe, wardrobe_want)status != disposed 的单品数 >= insight_item_delta_threshold(默认 20)
  3. 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() 标准化处理,统一输出包含 idmemorypreview(截断至 120 字符)、categorycategory_labelscore、时间戳等字段的结构化对象。

离线洞察分析也可通过内部 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 提供机器可读的精确参数,Mem0 提供人类可读的偏好语境。

<a id="19-yong-hu-dong-cha-fen-xi"></a>

---

用户洞察分析

用户洞察分析是 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_threshold20距上次 outfit 洞察新增穿搭触发数
insight_item_delta_threshold20距上次 item 洞察新增衣橱单品触发数
insight_chat_interval_days30距上次 chat 洞察的最小间隔天数
insight_merge_history_days730merge 时 outfit/chat 历史窗口(2年)
insight_tone_history_days730tone 汇总历史窗口(2年)
insight_chat_overlap_messages5对话回溯冗余消息数
insight_scheduler_cron0 0 *每日零点执行
insight_scheduler_timezoneAsia/Shanghai调度时区
insight_batch_size200每批扫描用户数
insight_user_concurrency5同时处理用户并发数

配置类还提供了 batch_day_window() 方法,用于将 UTC 时间转换为调度时区的当日窗口(用于幂等写入去重)。chat_intervalmerge_history_windowtone_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_outfitrun_itemrun_chat 三个布尔标志以及触发原因列表 reasons(取值为 outfit_deltaitem_deltachat_interval)。首次洞察的特殊处理:若用户从未做过洞察(对应维度的 last_*_atNone),则使用全量计数进行阈值比较——即新用户只要累计穿搭或单品数达到阈值即可触发。

衣橱单品的触发计数仅统计 source in ('wardrobe', 'wardrobe_want')status != 'disposed' 的新增记录,排除已丢弃的单品。

Sources: selectors.py

数据采集层

InsightCollector 封装了三个维度的原始数据抽取逻辑,每个方法接收 session 和时间窗口参数,返回结构化的字典负载。

穿搭数据采集

collect_outfit_data() 仅抽取自上次洞察以来新增的穿搭记录。采集链路为:outfits → 关联 cover_imageoutfit_items → 组成 items → 单品 images。返回的每条记录包含基础字段(idcreated_atsourcestatustitle)、行为信号(view_countliked_atdisliked_at)、生成上下文(prompt_dataactionswear_city)以及完整的组成单品信息(品类、颜色、材质、风格描述和关联图片)。

衣橱数据采集

collect_item_data() 分析当前全量有效衣橱(而非仅新增单品)。过滤条件为 source in ('wardrobe', 'wardrobe_want')status != 'disposed'。每条记录包含完整的商品属性(品类、品牌、价格)和风格标签体系(item_descriptionvisual_aestheticssilhouette_and_cutdesign_and_detailsmaterialscoloroccasion_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.txtJSON 穿搭记录<csv>满足 outfit 触发
analyze_item()insight_item.txtJSON 衣橱数据<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 键,每项动作仅允许 actionmemory_idcontent 三个字段。

聊天负载渲染

render_chat_payload() 将消息列表转换为可读文本,每条消息包含时间戳、会话 ID、渠道、角色、消息类型等头部信息,并对结构化 jsonValue 进行层级展开——提取 typepayload_typepayload_textpayload_records 等关键字段,避免将原始 JSON 裸传给模型。

Merge 负载组装

render_merge_payload() 将四部分数据组装为分段文本:[CurrentMemories](当前 Mem0 记忆)、[OutfitInsights](近两年穿搭洞察)、[LatestItemInsight](最新衣橱洞察)、[ChatInsights](近两年对话洞察)。其中穿搭和对话使用历史全量窗口,衣橱仅使用最新一条

Sources: analyzers.py

提示词设计

洞察系统的四类提示词均位于 data/prompts/memory/ 目录,采用中英文双语版本。每份提示词定义了严格的角色定位、分析规则和输出协议。

维度分析提示词

三个维度分析提示词(insight_outfitinsight_iteminsight_chat)共享相同的输出约束——仅允许输出 <csv>,格式为 Dimension, Confidence, Observation 三列。各维度允许的 Dimension 字段范围不同:

维度允许的 Dimension 字段
穿搭 (outfit)Style_Volatility_IndexAesthetic_TargetsVTO_Positive_TriggersVTO_Negative_Triggerspersonal_tabooscultural_taboos
衣橱 (item)Color_Palette_DistributionSignature_PiecesBlind_SpotWardrobe_BalanceBrand_AffinityPrice_Range
对话 (chat)flatter_areashighlight_areaspersonal_tabooscultural_taboosAesthetic_TargetsSignature_PiecesBlind_SpotBrand_AffinityPrice_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() 解析后执行严格校验:

差量执行

apply_actions() 执行实际的 Mem0 写入。执行前会校验所有 update/delete 动作的 memory_id 属于当前用户(通过比对 current_ids 集合)。写入操作使用 agent_id=AGENT_ID(即 "outfit-agent")隔离作用域。若 Mem0 未启用,仅记录动作但不实际执行,返回 memory_enabled=False

执行完成后返回统计结果,包含 add_countupdate_countdelete_countcurrent_memory_count

Sources: mem0_sync.py, ops.py

持久化与数据模型

user_insights 表

洞察数据存储在 user_insights 表中,结构极简但足够支撑完整的洞察生命周期:

字段类型说明
user_idBIGINT NOT NULL用户 ID
categoryVARCHAR(32) NOT NULL取值:outfititemchattonemerge
updated_atTIMESTAMPTZ NOT NULL洞察生成时间
contentTEXT NOT NULLLLM 分析结果原文

支持五种 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 表和相关索引(包括 outfitsitemsmessages 表的辅助索引)。该函数在 app/main.py 的启动事件中被调用。

Sources: repo.py, 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_usersqueued_userscompleted_usersfailed_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_idcategoryq(全文搜索)过滤
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.idstatustrigger_reasonran_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>

---

穿搭工作流服务

穿搭工作流服务是系统中负责从用户需求到完整穿搭结果全链路自动化执行的核心业务模块。它基于 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 映射为 outerwearshoe 映射为 shoes 等别名统一。

字段类型说明
task_textstr用户穿搭需求的文本描述,不可为空
scenedict场景信息(场合、城市、天气等)
constraintsdict约束条件(颜色、预算等)
style_preferenceslist[str]风格偏好列表
user_profile_noteslist[str]用户画像相关备注
must_have_itemslist[WorkflowRequiredItem]必选单品,最多 8 项
metadatadict元数据

OutfitWorkflowRequest 是构建后的完整工作流请求,除了包含 OutfitTaskPayload 外,还携带 run_idworkflow_session_iduser_id、渠道信息和用户头像路径。其中 workflow_session_idagent_session_idrun_id 拼接而成,用于独立的 Agno 会话持久化。

Sources: models.py, models.py, models.py

中间产物模型

工作流执行过程中产生的中间数据通过以下模型传递:

Sources: models.py

输出模型

WorkflowDispatchResult 是整个工作流的最终输出,包含:

字段类型说明
run_idstr本次工作流运行的唯一标识
statusstrcompleted / partial_failed / failed
outfitslist[WorkflowOutfitResult]全部穿搭结果
summary_text`str \None`LLM 生成的总结文案
failed_countint失败的穿搭套数
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. 1. 构建 LLM 请求:从 data/prompts/workflows/outfit_ideation_workflow_adapter.{lang}.txt 加载提示词模板,注入任务 JSON 和构思上下文。
  2. 2. 构建构思上下文:并行加载用户画像(来自 user_profiles 表)和相关记忆(来自 Mem0 向量检索),与 must_have_items 合并后作为额外上下文。
  3. 3. 调用 LLM:以 OutfitIdeationResponse 作为结构化输出约束,要求 LLM 返回恰好 2 套方案。
  4. 4. 必选单品保障:遍历生成的方案,将 must_have_items 中的固定单品(带 item_idimage_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() 为其匹配具体单品。选择逻辑分两条路径:

每个槽位最多返回 5 个候选,选择结果包含品类三级分类、价格、品牌等摘要信息。选择过程中通过 used_item_ids 集合确保同一穿搭内不重复使用单品。

Sources: outfit_generation.py, outfit_item_selection.py

Step 4:generate_images(图片生成)

使用 asyncio.gather 并行为每套穿搭调用 _generate_single_outfit()。该方法的核心流程:

  1. 1. 构建 add_items:将 SelectedItem 列表转换为包含 item_idposition(标准化后的槽位名)、layer_orderis_required 的结构。
  2. 2. 构建 prompt_data_overrides:注入穿搭任务、创意、场景、约束和选中单品等上下文到图片生成提示词中。
  3. 3. 调用 generate_outfit_by():这是底层的穿搭图生成函数,负责调用图片生成服务并持久化结果。关键参数包括 is_async=False(同步执行)和 invoke_completion_callbacks=False(不触发异步回调,由工作流统一处理分发)。
  4. 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 是工作流的编排中心,以单例模式运行,承担请求构建、工作流执行、状态持久化和中断恢复四项职责。

请求构建与执行

服务提供两种执行模式:

两种模式都通过 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_stageoutfit_status
成功完成s3_visual_deliveredcompleted
部分失败s3_visual_deliveredpartial_failed
执行异常outfit_failedfailed
用户新消息打断await_outfit_confirmationawaiting_confirmation
被取消 (CancelledError)await_outfit_confirmationawaiting_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. 1. 工作流已完成COMPLETED):直接将结果状态写回对话状态,跳过等待。
  2. 2. 工作流失败/取消ERROR / FAILED / CANCELLED):将状态回退到 await_outfit_confirmation,提示用户重试。
  3. 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 的状态不在 COMPLETEDERRORCANCELLEDPAUSED 之中时才认为是活跃的。

Sources: outfit_workflow_tools.py

任务参数解析

_normalize_task_payload() 支持三种参数来源的合并:

  1. 1. dialog_state.outfit_task:已有对话状态中的基础任务
  2. 2. outfit_task_json:显式传入的完整任务 JSON,覆盖基础任务
  3. 3. task_text / must_have_items_json:单独覆盖的任务文本和必选单品

所有来源通过 normalize_outfit_task_payload_data() 做统一标准化,包括场景文本解析、列表去重、必选单品按槽位合并等。

Sources: outfit_workflow_tools.py, models.py

执行后状态更新

工具执行完成后,通过 update_dialog_state_fields() 更新对话状态,并返回包含以下关键字段的 JSON:

这个机制通过工具元属性 __tool_stop_after_tool_call_if__ 实现,当 suppress_main_agentTrue 时,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. 1. 调用 OutfitIdeationService 生成 2 套创意
  2. 2. 为每套创意调用 LLM 生成图片提示词
  3. 3. 调用 generate_image() 生成图片
  4. 4. 返回包含 ideasimagesoutfits 的结果

这个模式主要用于内部测试 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_outfitssend_text_message)需要一个模拟的 RunContextbuild_internal_run_context() 通过 SimpleNamespace 构建一个轻量级上下文,包含 session_idrun_iduser_id 和注入了 tool_ctxsession_state,使得消息发送工具能够正常工作。

Sources: runtime.py

与其他模块的关系

穿搭工作流服务在系统中的位置可参考以下页面:

<a id="21-tu-pian-sheng-cheng-yu-chu-li"></a>

---

图片生成与处理

图片生成与处理系统是 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_concurrency3最大并发生成任务数
poll_interval1.0s队列轮询间隔
db_reconcile_interval_seconds180s数据库对账间隔
db_batch_size100单次数据库查询批次大小
queued_ttl_seconds600s队列任务过期时间
processing_lock_ttl_seconds1800s处理锁过期时间

初始化时,系统会注册 on_outfit_completed 回调函数,用于在图片生成完成后执行渠道投递和消息持久化。这些配置参数通过 Settings 对象从环境变量或配置文件中读取,允许根据部署环境进行调整。

Sources: app/img_gen_handler.py, app/config.py, app/main.py

轮询与调度机制

ImgGenHandler 的核心是一个异步轮询循环 _poll_loop,该循环持续运行直到收到停止信号。轮询循环的工作流程如下:

  1. 1. 定期对账:每 db_reconcile_interval_seconds 秒执行一次数据库对账,将 status=pending 的穿搭记录同步到 Redis 阘列
  2. 2. 任务获取:从 Redis 队列中弹出一个待处理的 outfit_id,使用阻塞弹出操作(brpop)避免忙等待
  3. 3. 任务分发:为每个 outfit_id 创建一个异步任务,通过信号量控制并发数
  4. 4. 处理锁获取:在开始处理前获取 Redis 分布式锁,防止重复处理
  5. 5. 状态更新:将穿搭状态从 pending 更新为 generating,生成完成后更新为 completedfailed

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.txtLLM 生成专业提示词
单品编辑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. 1. 检查是否已有处理锁存在,若存在则跳过
  2. 2. 尝试设置已入队标记(SET NX),若已存在则跳过
  3. 3. 将 outfit_id 推入队列
  4. 4. 若推入失败,清理已入队标记并抛出异常

这种设计确保了在分布式环境下,即使多个实例同时尝试入队同一穿搭,也只会有一个成功入队。

Sources: app/img_gen_queue.py

Victor Prompt Builder:智能提示词构建

Victor Prompt Builder 是图片生成系统的提示词工程核心,负责将用户画像、单品信息和穿搭场景转换为高质量的生成提示词。该组件通过 LLM 驱动的 "Director" 角色,生成专业的摄影指导提示词。

上下文构建

build_victor_context 函数收集三类关键信息构建提示词上下文:

用户画像user_profile):从用户资料中提取性别、体型、身高体重、肤色、发型、脸型等外貌特征,以及城市、职业、风格偏好等个人信息。这些信息帮助生成模型理解用户的整体形象。

穿搭数据outfit_data):包含穿搭标题、穿着城市、造型师文案、单品列表等。每个单品包含 ref_idproduct_typebrandproduct_description 等结构化信息。

头像参考avatar_reference):指向用户头像图片的引用标识,用于在生成过程中保持人物特征的一致性。

提示词生成流程

generate_victor_final_prompt 函数执行完整的提示词生成流程:

  1. 1. 构建上下文数据结构
  2. 2. 加载 Director 提示词模板(victor_director.txt
  3. 3. 将上下文 JSON 嵌入到提示词中
  4. 4. 调用 LLM 生成专业的摄影指导文本
  5. 5. 追加参考图片说明(victor_reference_append.txt
  6. 6. 返回最终的生成提示词

LLM 生成的提示词通常包含光线、构图、角度、风格等专业摄影要素,确保生成的穿搭效果图具有商业级质量。

Sources: app/victor_prompt_builder.py, app/victor_prompt_builder.py

图像后处理系统

Generated Image Postprocess 模块负责对生成的图像进行标准化处理,确保输出图像符合系统要求的质量标准。

比例裁剪

系统使用 _crop_image_bytes_to_ratio 函数实现智能比例裁剪:

  1. 1. 解析目标比例(如 "3:4"、"1:1")
  2. 2. 计算源图像与目标比例的相似度
  3. 3. 若相似度误差超过阈值(3%),执行中心裁剪
  4. 4. 裁剪时保持图像中心区域不变,裁剪多余部分
  5. 5. 保存裁剪后的图像,保持原始格式

裁剪操作使用 PIL 库实现,支持 JPEG、PNG、WebP 等格式。对于 JPEG 格式,输出质量设置为 100(无损),确保图像细节完整保留。

水印添加

系统根据图像类型自动添加相应的水印:

图像类型水印类别说明
TRYON_REFtryon虚拟试穿图像
VIBE_REFoutfit穿搭参考图像
其他不添加水印

水印通过 apply_ai_watermark 函数添加,支持自定义水印内容和位置。

元数据存储

后处理完成后,系统会将裁剪元数据编码到图像的 user_note 字段中,包含以下信息:

这些元数据用于后续的图像管理和质量追溯。

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:

参数说明

处理流程

  1. 1. 从 ToolContext 获取用户信息
  2. 2. 校验提示词和参考图片
  3. 3. 调用业务层图片生成 API
  4. 4. 执行图像后处理
  5. 5. 自动发送生成的图片到当前渠道
  6. 6. 返回包含 image_idoss_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:

参数说明

处理流程

  1. 1. 从 ToolContext 获取用户头像路径
  2. 2. 调用虚拟试穿 API,传入穿搭图和头像
  3. 3. 支持基于先前试穿结果进行微调
  4. 4. 执行图像后处理
  5. 5. 自动发送生成的试穿图到当前渠道

Sources: app/tools/image_gen_tools.py, app/tools/image_gen_tools.py

图片入站处理管道

Image Pipeline 模块提供统一的图片入站处理流程,确保用户上传的图片经过标准化处理后进入系统。

处理流程

run_image_pipeline 函数执行完整的入站处理:

  1. 1. 内容审核:调用 ensure_image_content_allowed 检查图片内容是否符合平台规范
  2. 2. 重复检测:使用图像向量相似度检测是否为重复图片(阈值 0.97)
  3. 3. 图片保存:将图片上传到 OSS 并保存元数据到数据库
  4. 4. 语义描述:调用图片描述服务生成图片的文本描述
  5. 5. 向量计算:计算图片的语义向量用于后续相似度搜索

重复图片处理

当检测到重复图片时(相似度 > 0.97),系统会:

这种设计既节省了存储空间,又避免了重复处理带来的计算开销。

Sources: app/messaging/image_pipeline.py

图片描述与向量化

Image Caption Handler 是一个后台工作器,负责为没有描述的图片异步生成语义描述和向量表示。

工作机制

该组件采用与 ImgGenHandler 类似的架构:

  1. 1. 定期对账:每 180 秒扫描数据库,找出需要生成描述的图片
  2. 2. 队列管理:使用 Redis 队列管理待处理的图片 ID
  3. 3. 并发控制:通过信号量限制并发处理数(默认 2)
  4. 4. 处理锁:使用 Redis 分布式锁防止重复处理

处理流程

对每张图片的处理流程如下:

  1. 1. 从 OSS 获取图片字节
  2. 2. 调用图片描述服务生成文本描述
  3. 3. 更新数据库中的 description 字段
  4. 4. 计算描述文本和用户备注的向量表示
  5. 5. 保存向量到数据库的 description_vec 字段

这些向量用于支持基于语义的图片搜索功能,用户可以通过自然语言描述找到相关的穿搭图片。

Sources: app/image_caption_handler.py

完成回调系统

ImgGen Callback 系统负责在图片生成完成后执行后续处理,包括渠道投递、消息持久化和摘要生成。

回调触发时机

回调在以下情况触发:

回调处理流程

on_outfit_completed 函数执行完整的回调处理:

  1. 1. 获取回调锁:使用 Redis 分布式锁确保同一消息的回调不会并发执行
  2. 2. 加载回调状态:从 Redis 加载之前的回调状态
  3. 3. 检查终止状态:若已处于终止状态则跳过
  4. 4. 获取消息上下文:从数据库加载消息和会话信息
  5. 5. 收集完成结果:查询所有已完成和失败的穿搭记录
  6. 6. 按顺序投递:按照生成顺序投递穿搭图片到渠道
  7. 7. 持久化消息:将穿搭消息保存到数据库
  8. 8. 生成摘要:调用 Agent 生成穿搭摘要文本
  9. 9. 发送摘要:将摘要文本发送到渠道

渠道适配

回调系统支持两种主要渠道的差异化处理:

微信客服WECHAT_KEFU):

微信小程序WECHAT_MINIAPP):

Sources: app/img_gen_callback.py, app/img_gen_callback.py

配置参数参考

图片生成系统的配置参数定义在 Settings 类中,支持通过环境变量或配置文件进行调整:

配置项默认值说明
img_gen_max_concurrency3图片生成最大并发数
img_gen_queue_pop_timeout_seconds1队列弹出超时时间(秒)
img_gen_db_reconcile_interval_seconds180数据库对账间隔(秒)
img_gen_db_batch_size100数据库对账批次大小
img_gen_queue_queued_ttl_seconds600队列任务 TTL(秒)
img_gen_processing_lock_ttl_seconds1800处理锁 TTL(秒)
image_caption_max_concurrency2图片描述生成并发数
image_caption_poll_interval_seconds1.0图片描述轮询间隔(秒)
image_caption_db_reconcile_interval_seconds180图片描述对账间隔(秒)
image_caption_batch_size50图片描述对账批次大小
image_caption_queue_queued_ttl_seconds600图片描述队列 TTL(秒)
image_caption_processing_lock_ttl_seconds1800图片描述处理锁 TTL(秒)

这些参数需要根据实际部署环境的负载情况进行调整。高并发场景下,建议适当增加 img_gen_max_concurrencyimage_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. 1. 头像参考:将用户头像作为参考图片传入生成模型
  2. 2. 提示词增强:在 Victor Prompt 中包含用户的外貌描述
  3. 3. 体型适配:根据体型选择合适的模型图片(普通/Plus)
  4. 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 函数支持以下搜索维度:

搜索流程

  1. 1. 从数据库加载参考图片
  2. 2. 获取参考图片的向量表示(image_vec
  3. 3. 若无向量,使用 OSS 路径计算向量
  4. 4. 调用相似度搜索 API
  5. 5. 返回匹配结果列表,包含 image_idoss_urlscore 等信息

该工具可用于查找灵感图片、相似穿搭、单品参考等场景。

Sources: app/tools/image_search_tools.py

测试与调试

图片生成系统的测试覆盖了核心流程的各个环节,主要测试文件包括:

单元测试

test_img_gen_flow.py:包含图片生成流程的完整测试,覆盖:

测试策略

测试使用 Fake 对象模拟外部依赖:

通过 monkeypatch 替换真实的外部调用,确保测试的隔离性和可重复性。

调试建议

  1. 1. 查看生成提示词:在 trace 中检查 victor_prompt span 的输入输出
  2. 2. 监控队列状态:使用 Redis CLI 查看队列长度和处理锁状态
  3. 3. 检查回调状态:查看 Redis 中的 img_gen:callback:state:{msg_id}
  4. 4. 分析后处理日志:搜索 "Generated image postprocess metrics" 日志条目

Sources: tests/test_img_gen_flow.py, tests/test_img_gen_flow.py

相关文档

<a id="22-dan-pin-chu-shi-hua-chu-li"></a>

---

单品初始化处理

单品初始化处理是穿搭系统中的核心后台服务模块,负责在用户保存衣物后异步完成标准化图片生成和属性补全。该系统采用 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 故障等原因遗留在 pendingfailed 状态的单品重新入队。这种双通道设计确保了任务不会因为瞬时故障而永久丢失。

Sources: item_init_handler.py, config.py

队列数据结构

Redis 队列层使用三种键类型实现完整的任务生命周期管理,每种键承担不同的职责:

键模式类型用途TTL
queue:outfit:item_initList任务队列,存储待处理的 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_statusinit_error 追踪每个单品的处理状态。状态更新在每个阶段转换时执行,确保即使进程崩溃也能通过数据库对账恢复正确的状态。

错误处理采用状态标记 + 延迟重试策略:当某个阶段执行失败时,系统将状态标记为 failed 并记录错误信息(截断至 500 字符)。数据库对账机制会定期扫描 failed 状态的单品,如果距离上次更新已超过重试延迟(默认 300 秒),则将其重新入队。同时,系统还会处理"僵死"任务——处于 normalizingvlm_parsing 状态超过 1800 秒的单品会被视为处理超时,同样会被重新入队。

状态含义触发条件
pending待处理单品保存时设置
normalizing标准图生成中claim_item 成功后
vlm_parsingVLM 属性解析中标准图生成完成
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_concurrency2最大并发处理数
item_init_poll_interval_seconds1.0轮询间隔(秒)
item_init_db_reconcile_interval_seconds180数据库对账间隔(秒)
item_init_batch_size20单次对账批量大小
item_init_retry_delay_seconds300失败重试延迟(秒)
item_init_stale_after_seconds1800僵死任务超时(秒)
item_init_queue_queued_ttl_seconds600去重标记 TTL(秒)
item_init_processing_lock_ttl_seconds1800处理锁 TTL(秒)

Sources: config.py

可观测性

系统集成了 OpenTelemetry 追踪,每个处理任务都会创建一个名为 Item.enrichment.background 的追踪 Span,记录单品 ID、用户 ID、来源、初始状态等上下文信息。处理完成后,Span 会附加最终状态信息。这种端到端的追踪能力使得运维团队可以快速定位处理延迟或失败的根因。

此外,系统在关键节点输出结构化日志,包括任务分发、阶段开始/完成、错误详情等,支持日志聚合分析和告警配置。

Sources: item_init_handler.py, item.py

与其他模块的关系

单品初始化处理与以下模块存在紧密的协作关系:

<a id="23-dashboard-guan-li-mian-ban"></a>

---

Dashboard 管理面板

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/reactTOML / 文本编辑(Agent、Skill、Prompt)
路由React Router v7前端路由,basename=/dashboard
状态管理Zustand + persistAdmin 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链路追踪SearchSession 列表、实时运行监控、Run 工具调用树、dialog_state
/auto-tests自动化测试Experiment自然语言规则转 JSON、测试用例执行
/playgroundPlaygroundExperimentPrompt 调试沙箱、历史导入、实时对话测试
/agentsAgent 配置Robot主 Agent TOML 编辑、热重载、可用工具/Skills 展示
/skillsSkill 管理ToolSkill TOML 编辑、创建/删除、热重载
/prompts数据资产FileText资源树浏览、Monaco 编辑、历史版本回滚、灰度预览
/settings系统配置Setting运行时配置查看与受限更新、模型灰度规则
/ossOSS 管理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 的发现机制遍历 data/skills/ 目录下的所有 skill.toml 文件,并与运行时已加载的 Skill 注册表进行对比,区分「已加载」和「仅存在文件」两种状态。

Sources: SkillConfig.tsx, skills.py

模块 C:数据资产(资源中心)

资源中心是 Dashboard 中功能最丰富的模块之一。它自动发现 outfit_agent/dataoutfit_biz/data 两个数据根目录下的所有资源文件,按分类(Agent/Team、Skill、Prompt、Channel、I18n、Tool、Asset 等)构建树形浏览结构。

资源文件支持两个数据源的覆盖层机制:当 outfit_agent/dataoutfit_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_tracesagno.agno_spans 表,展示今日请求总数、平均响应时间、工具调用分布(Top 10 柱状图)和 Agent 运行分布(饼图)。

错误日志:通过内存环形缓冲区(_AdminLogHandler)捕获最近的 WARN/ERROR 级别日志,支持按级别过滤。

页面每 30 秒自动刷新,Header 区域也独立每 30 秒检测服务健康状态。

Sources: Monitor.tsx, monitor.py

模块 F:链路追踪

链路追踪是排障的核心工具,提供三层钻取视图:

  1. 1. Session 列表:分页展示所有会话,支持按 user_id、session_id、agent_id、status 过滤。每条记录展示最新用户消息、Agent 回复、trace 数量和错误数量。
  2. 2. Session 详情:点击进入后展示该会话下所有 Run 的摘要,包括 LLM 和工具调用统计。
  3. 3. Run 详情:展示单次 Run 的完整 span 树,包含 LLM 请求详情(token 统计)、工具调用详情(参数、结果、耗时)。

此外还提供实时运行监控面板,每 4 秒轮询当前进程内正在执行的 Agent Run,展示关键的运行态字段:

字段说明
active_skill当前激活的技能
loaded_skills已加载的技能列表
active_scene当前场景
sop_stageSOP 阶段
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_enabledMem0 记忆开关
mem0_search_limitMem0 搜索条数限制
litellm_redis_cache_enabledLiteLLM Redis 缓存开关
litellm_redis_ttl缓存 TTL
wechat_miniprogram_title小程序标题
wechat_miniprogram_pagepath小程序跳转路径

页面同时集成模型灰度规则管理,支持按用户维度灰度切换不同模型配置。

Sources: SystemSettings.tsx, settings.py

模块 H:数据管理

数据管理页面提供业务数据的全局视图,分为三个 Tab:

页面还提供向量批量刷新功能,可对缺失向量的单品触发重新计算。

Sources: DataManager.tsx, data.py

模块 I:用户管理

用户管理页面提供用户全景视图,包括:

Sources: UserManager.tsx, users.py

模块 J:消息管理

消息管理页面整合了三个子功能:

Sources: MessageManagement.tsx, message_management.py, access_control.py, interception.py

模块 K:OSS 管理

OSS 管理页面提供对象存储的文件管理能力:

支持的上传文件类型包括:jpg、jpeg、png、gif、webp、pdf、txt、json、mp4、mov。

Sources: OssManager.tsx, oss.py

模块 L:Playground 测试沙箱

Playground 提供一个隔离的 Prompt 调试环境,用于快速验证 Agent 行为:

Sources: Playground.tsx, playground.py

模块 M:自动化测试

自动化测试页面支持自然语言驱动的测试用例编写与执行:

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/tracesSession 列表/详情、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. 1. 查看实时运行状态:在「链路追踪」页面的实时 Run 面板中,查看当前 session 的 active_skillsop_stagepending_confirmation 等字段,确认 Agent 是否停在正确的 SOP 阶段。
  2. 2. 检查 dialog_state:在 Run 详情中查看 dialog_state JSON,确认阶段推进是否正确,是否有工具被门控拒绝。
  3. 3. 查看用户消息与回复:在 Session 详情中查看最新用户消息和 Agent 回复,定位对话卡点。
  4. 4. 检查系统监控:在「系统监控」页面查看服务健康状态和最近错误日志,排除基础设施故障。
  5. 5. 检查配置与 Skill:在「Agent 配置」和「Skill 管理」页面确认当前运行的配置是否正确,是否有未生效的修改需要热重载。
  6. 6. 检查灰度规则:在「系统配置」的模型灰度区域或「数据资产」的灰度预览中,确认目标用户是否命中了预期的灰度覆盖。

下一步阅读

<a id="24-admin-api-jie-kou"></a>

---

Admin API 接口

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

Sources: auth.py, config.py

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
链路追踪/tracesSession/Run 列表与详情、实时 Run、生图重试traces.py
监控/monitor服务健康检查、今日统计、错误日志monitor.py
Playground/playground模拟对话调试、导入历史、Prompt 覆盖playground.py
用户/users用户总览、详情、按范围清理数据users.py
数据/data单品/穿搭/图片概览与详情、质量检查data.py
记忆/memoriesMem0 记忆查询/摘要/编辑/删除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 定义。

端点方法说明
/agentsGET列出主 Agent 配置信息、可用 tools/skills、最近重载时间
/agents/content/{agent_id}GET读取主 Agent TOML 原始内容(仅支持 __main_team__
/agents/content/{agent_id}PUT更新 TOML 文件(自动校验 TOML 语法)
/agents/reloadPOST热重载主 Agent 与所有 Skills

重载操作会同步调用 app.agent.reload_agents(),返回重载耗时(毫秒)。API 不接受任意 agent_id,仅支持固定的虚拟 ID __main_team__

Sources: agents.py

技能管理 (/skills)

管理 data/skills/*/skill.toml 下的技能定义,支持完整的 CRUD 和热重载生命周期。

端点方法说明
/skillsGET列出所有 Skill(含已加载和未加载),展示 tools、scripts、references
/skills/exampleGET获取示例 skill.toml 模板
/skills/content/{skill_id}GET读取指定 Skill 的 TOML 内容
/skills/content/{skill_id}PUT更新 Skill TOML
/skillsPOST创建新 Skill(传入 skill_id 查询参数 + TOML 内容体)
/skills/{skill_id}DELETE删除 Skill TOML 文件
/skills/reloadPOST热重载所有 Skill 定义

Skill ID 支持连字符和下划线两种格式,查找时会自动尝试两种变体。

Sources: skills.py

运行时配置 (/settings)

查看和更新运行时配置项。响应中敏感字段(如 wechat_secretadmin_secret_key、数据库连接串等)会被脱敏为 ****

端点方法说明
/settingsGET获取当前配置(含模型角色映射、Agent 参数、可变字段列表)
/settingsPUT更新可变配置项

可变字段(无需重启):mem0_enabledmem0_search_limitlitellm_redis_cache_enabledlitellm_redis_ttlwechat_miniprogram_titlewechat_miniprogram_pagepath

需重启字段(更新后标记提示):litellm_redis_cache_enabledlitellm_redis_ttl

其他字段不允许通过 API 更新,返回 422

Sources: settings.py

工具描述管理 (/tool-descriptions)

管理 data/tools/descriptions.toml 文件,控制每个工具面向 LLM 的描述文案。

端点方法说明
/tool-descriptionsGET获取文件元信息(大小、修改时间、工具列表)
/tool-descriptions/contentGET读取 TOML 内容
/tool-descriptions/contentPUT更新 TOML 内容(自动校验语法)
/tool-descriptions/reloadPOST热重载工具描述到运行时
/tool-descriptions/historyGET获取历史版本列表
/tool-descriptions/history/{version}GET读取指定历史版本内容
/tool-descriptions/rollbackPOST回滚到指定版本

每次更新会自动备份旧版本,最多保留 10 个历史版本。编辑后需调用 /reload 才能使运行时生效。

Sources: tool_descriptions.py

灰度配置 (/gray)

管理基于数据库的灰度规则,支持按用户 ID、表达式进行提示词、JSON、文本、模型、Agent 和 Skill 的灰度覆盖。

端点方法说明
/gray/rulesGET列出灰度规则(支持按 namespace/path/lang/status 过滤)
/gray/queryGET按 scope 查询(支持 resource_id 快速定位)
/gray/rules/{rule_id}GET获取单条规则详情
/gray/rulesPOST创建灰度规则
/gray/rules/{rule_id}PUT更新灰度规则
/gray/rules/{rule_id}DELETE删除灰度规则
/gray/previewPOST按用户预览资源灰度命中结果
/gray/model-previewPOST按用户预览模型灰度命中结果
/gray/cache/refreshPOST刷新灰度运行时缓存

灰度规则支持的 namespace:promptjsontextmodelagentskillusers_expr 字段支持 *(全部用户)或用户 ID 表达式。merge_mode 支持 replacejson_mergedict_merge 三种合并模式。

Sources: gray.py

资源管理

资源中心 (/prompts)

自动发现 outfit_agent/dataoutfit_biz/data 两个数据根目录下的所有文件,按分类展示,并支持文本资源的编辑和版本管理。该模块是 Dashboard "资源中心"页面的核心 API 层。

资源分类体系:

分类(Section)包含内容排序权重
Agent / Teamagents/team/ 目录下的文件0
Skillskills/ 目录下的文件1
Promptprompts/ 目录下的各类提示词2
Channelchannels/ 目录下的渠道文本3
I18ni18n/ 目录下的多语言文案4
Tooltools/ 目录下的工具资源5
Assetassets/fonts/ 目录下的素材6
Other其他文件7
端点方法说明
/prompts/listGET列出所有资源文件(含 overlay 状态、可编辑标记)
/prompts/contentGET读取资源内容或元信息(支持 id 和兼容旧版 path
/prompts/contentPUT更新资源文件内容(自动备份旧版本)
/prompts/clear-cachePOST清除 PromptLoader / i18n 文本缓存
/prompts/historyGET获取资源版本历史列表
/prompts/history/contentGET读取指定历史版本内容
/prompts/rollbackPOST回滚资源到指定版本

资源 ID 格式为 source:path(如 agent:prompts/agents/main_team.zh.txtbiz:i18n/system.zh.json)。当 outfit_agentoutfit_biz 中存在同名文件时,agent 层标记为 overrides(覆盖),biz 层标记为 shadowed(被覆盖),实现了类似 overlay 文件系统的语义。

更新后 API 返回 recommended_action 字段,提示前端应执行的热应用操作(如 reload_agentsreload_skillsreload_tool_descriptions)。

Sources: prompts.py

统一文件管理 (/content-files)

/prompts 类似但更通用的资源管理接口,额外支持二进制文件替换、原始文件下载和显式热应用动作。

端点方法说明
/content-files/listGET列出 data 目录下所有可管理文件(按分组排序)
/content-files/contentGET读取文件内容或元信息
/content-files/contentPUT更新文本文件内容
/content-files/replacePOST替换图片等二进制资源(multipart 上传)
/content-files/applyPOST按文件类型执行热应用动作
/content-files/historyGET获取文件历史版本
/content-files/history/contentGET读取指定历史版本内容
/content-files/rollbackPOST回滚文件到历史版本
/content-files/rawGET原始文件下载(返回 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/sessionsGET分页列出 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-runsGET获取当前进程内正在运行的 agent run 列表
/traces/live-runs/{run_id}GET获取当前进程内单个实时 run 详情
/traces/runs/{run_id}/retry-image-generationPOST重新入队指定 run 的生图任务

Session 详情中包含 dialog_state 的关键字段:active_skillloaded_skillsstagesop_stagepending_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/statusGET各服务健康状态(PostgreSQL、Redis、OSS、Mem0、Agent)+ 运行时信息
/monitor/statsGET今日统计(请求数、平均响应时间、工具分布、Agent 路由分布)
/monitor/logsGET最近错误/警告日志(内存环形缓冲区,最多 500 条)

健康检查通过异步并发执行 5 项检测(_check_database_check_redis_check_oss_check_mem0_check_agent),所有检查通过时顶层 oktrue

统计信息基于 agno.agno_tracesagno.agno_spans 表的当日数据聚合,按 Agent/Team 和工具名称分别展示分布。

Sources: monitor.py

Playground 调试 (/playground)

在管理后台直接模拟 Agent 对话,用于调试 Prompt 效果和验证配置变更。

端点方法说明
/playground/configGET获取 playground 默认配置与当前 Prompt
/playground/import-historyPOST按 session_id 导入已有历史消息
/playground/runPOST运行调试对话(支持 Prompt 覆盖、渠道切换、历史注入)

Playground 运行时创建独立的 trace session(前缀 playground:),支持覆盖主 Prompt 和渠道 Prompt,可用于 A/B 对比不同 Prompt 版本的效果。channel 参数支持 wechat_miniappwechat_kefu 两种渠道模式。

Sources: playground.py

用户与数据运维

用户管理 (/users)

端点方法说明
/users/summaryGET用户管理总览(总用户数、活跃用户、按渠道分布、数据完整性检查)
/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/overviewGET数据管理总览(单品/穿搭/图片统计与质量检查)
(更多端点)单品列表与详情、穿搭列表与详情、图片列表与详情、向量刷新、文本/图片搜索

数据概览包含质量检查指标:缺失向量、缺失描述、归属不匹配、孤儿记录等。单品和图片支持文本搜索和以图搜图两种检索方式。

Sources: data.py

记忆管理 (/memories)

端点方法说明
/memoriesGET分页列出 Mem0 记忆(支持 q/user_id/run_id/category 过滤)
/memories/summaryGET记忆摘要(按分类统计)
(更多端点)记忆编辑/删除等

记忆按分类标签过滤,支持的 category 包括:外观偏好、风格偏好、场景偏好、体型信息、品牌偏好、色彩偏好、禁忌/敏感、对话风格等。

Sources: memories.py

用户洞察 (/insights)

端点方法说明
/insights/summaryGET洞察摘要(总记录数、用户数、按分类统计)
/insightsGET分页查询洞察历史(支持 user_id/category/q 过滤)
/insights/users/{user_id}/latestGET获取指定用户各分类的最新洞察快照

洞察数据由离线调度任务(insight_scheduler_cron,默认每日零点)自动生成,存储在 user_insights 表中。

Sources: insights.py

会话消息 (/message-management)

端点方法说明
/message-management/sessionsGET会话消息列表(支持 channel/q 过滤,含最新消息预览)
/message-management/sessions/{conversation_id}/messagesGET会话消息详情(分页,支持 before_id 前向翻页)

会话列表的搜索支持按 session_id、用户昵称、标题和最新消息内容进行模糊匹配。

Sources: message_management.py

OSS 文件管理 (/oss)

端点方法说明
/oss/listGET列出 OSS 文件(按 prefix 前缀浏览)
/oss/signed-urlGET获取签名预览 URL
/oss/objectDELETE删除单个文件
/oss/batch-deletePOST批量删除文件
/oss/mkdirPOST创建文件夹
/oss/uploadPOST上传文件
/oss/upload-zipPOSTZIP 批量上传(自动解压)
/oss/folder-infoGET查询文件夹文件数
/oss/folder-deletePOST递归删除文件夹
/oss/folder-copyPOST复制或移动文件夹
/oss/copyPOST复制或移动单个文件

Sources: oss.py

内测统计 (/beta-users)

端点方法说明
/beta-users/dashboardGET内测用户实时统计面板

统计面板包含:用户激活时间、使用次数、工作流触发次数、试穿次数、单品上传数、画布进入/生成/修改次数、负面反馈、修改意图分析等指标,支持按 A/B 分组聚合。

Sources: beta_users.py

安全与准入

消息准入控制 (/access-control)

端点方法说明
/access-control/summaryGET消息准入控制总览
/access-control/policiesGET/POST准入策略列表 / 创建策略
/access-control/policies/{policy_id}PUT/DELETE更新 / 删除策略
/access-control/entriesGET/POST黑白名单列表 / 创建条目
/access-control/entries/{entry_id}PUT/DELETE更新 / 删除条目
/access-control/codesGET授权码列表
/access-control/codes/generatePOST批量生成授权码
/access-control/codes/{code_id}/disablePOST停用授权码

策略按 channel(渠道)和 service_account(客服账号)维度定义,支持 allowlist(白名单)、blocklist(黑名单)、code(授权码)三种策略类型。匹配类型支持 user_idunionidchannel + openid

Sources: access_control.py

消息拦截 (/interception)

端点方法说明
/interception/rulesGET/POST拦截规则列表 / 创建规则
/interception/rules/{rule_id}PUT/DELETE更新 / 删除规则
/interception/feedbackGET反馈/人工客服消息列表
/interception/feedback/{message_id}/processedPOST标记反馈已处理
/interception/feedback/{message_id}/replyPOST回复反馈消息

拦截规则支持精确消息匹配(exact_messages)、通配符匹配(wildcard_patterns)、正则匹配(regex_patterns)和用户画像条件匹配(profile_conditions)。触发后的动作类型包括 auto_reply(自动回复)等。

Sources: interception.py

自动化测试 (/auto-tests)

端点方法说明
/auto-tests/rules/translatePOST将自然语言规则描述转换为 JSON 规则
/auto-tests/executionsPOST执行自动化测试用例

该模块提供 LLM 辅助的规则翻译能力,将自然语言描述的测试断言转换为结构化的 AutoTestRule JSON,然后可在管理后台直接执行测试用例。

Sources: auto_tests.py

通用设计模式

错误响应格式

所有 Admin API 端点遵循 FastAPI 标准错误格式:


{

  "detail": "错误描述信息"

}

常见状态码:401(缺少 Authorization 头)、403(Token 无效或未配置)、404(资源不存在)、422(请求体校验失败或字段不允许更新)、500(服务端内部错误)。

版本历史与回滚

资源管理类端点(/prompts/content-files/tool-descriptions)共享相同的版本管理模式:

  1. 1. 自动备份:每次写入前将当前内容复制到历史目录,文件名使用 Unix 时间戳
  2. 2. 容量控制:最多保留 10 个历史版本,超出时自动删除最旧版本
  3. 3. 回滚流程:读取指定版本内容 → 备份当前版本 → 覆盖当前文件 → 清除运行时缓存

缓存清除

编辑配置后需要清除相应的运行时缓存才能生效:

各管理端点在保存后会根据资源类型自动执行对应的缓存清除操作。

配置管理层级关系


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.ymloutfit_biz/config.yml 之上进行深度合并(_deep_merge),agent 配置优先。Admin API 的 /settings 端点仅暴露有限的可变字段供运行时修改,敏感字段统一脱敏。

<a id="25-lian-lu-zhui-zong-yu-trace"></a>

---

链路追踪与 Trace

本页全面梳理 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_nametool.parameterstool.name 等),供聚合层按类型提取元数据。

Sources: traces.py, traces.py, traces.py

Span 类型推导

Admin API 在查询 span 树时,通过 _derive_span_type() 函数将原始 span_kindattributes 推导为统一的业务类型标签。这一推导逻辑优先使用 OpenInference 标准,然后回退到 agno 原始 kind,最后通过 attributes 特征识别。

推导类型判断条件典型场景
LLMopeninference.span.kind == "CHAIN" 不适用时,llm.model_name 存在或 span 名以 .ainvoke 结尾模型推理调用
TOOLtool.nametool.parameters 存在于 attributes工具/函数调用
AGENTopeninference.span.kind == "AGENT" 且无 agno.team.id;或 agno.agent.id / agent.name 存在单 Agent 执行
TEAMopeninference.span.kind == "AGENT"agno.team.id 存在;或 attributes 中有 agno.team.id / team.idTeam 协作委派
WORKFLOWopeninference.span.kind == "CHAIN"工作流编排(如穿搭生成流程)

每种类型的 span 在聚合时会提取不同的元数据字段:LLM 类型提取 modelproviderinput_tokensoutput_tokens;TOOL 类型提取 tool_nameparametersoutput;AGENT/TEAM 类型提取 agent_idteam_idrun_idsession_id。此外,所有类型的 span 都会扫描 workflow.outfit.dialog.skill.sop.* 前缀的 attributes 并提取为额外元数据。

Sources: traces.py, traces.py

实时追踪: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 包含:

工具钩子注册live_run_tool_hook 通过 _make_agent_tool_hooks() 注册为 Agent 的 tool_hooks。Agno 框架在每次工具调用时自动触发该钩子,钩子在调用前后分别执行 _start_tool_call_finish_tool_call,实现零侵入的工具级追踪。钩子兼容 Agno 的多种关键字参数(next_funcfuncfunctionfunction_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_KEYHyperDX 平台 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/sessionsGET分页列出 sessions,支持按 user_idsession_id(模糊)、agent_idteam_idstatus(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-runsGET当前 Redis 中活跃的 Live Run 列表,支持按 session_id 过滤
/traces/live-runs/{run_id}GET单个 Live Run 详情,包含完整 tool_calls 列表、session_state 快照
/traces/runs/{run_id}/retry-image-generationPOST重新入队指定 run 的生图任务,支持 reset_health 参数清空 upstream 健康惩罚

查询策略/traces/runs/{run_id} 端点采用双轨查询策略——首先在 agno_traces 中按 run_idtrace_id 查找持久化 trace;若未命中(例如 trace 尚未写入或来自非 OTel 通路),则回退到 agno_sessions.runs JSONB 数组中按 run_id 匹配 RunOutput payload,从中合成 span 树。这种设计确保无论数据来自哪条通路,都能返回完整结果。

关联 Run 发现:查询 run detail 时,系统会递归扫描 span 树的 input/output/meta 字段,提取所有 run_idchild_run_idworkflow_run_idoutfit_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 查询中暴露,使得排障时可以直接看到:

这种设计使得"排查问题时优先看 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 状态冲突。_TaskSafePostgresDbscoped_session 替换为普通 sessionmaker,确保每次导出调用获得独立的 Session。

Sources: agentos.py, agentos.py, agent.py

配置项

配置项默认值说明
live_run_redis_ttl_seconds180Live Run Redis 键 TTL(秒)
live_run_heartbeat_interval_seconds15Live Run 心跳刷新间隔(秒)
HYPERDX_API_KEYHyperDX 日志导出 API Key
OTEL_SERVICE_NAMEOTel 服务名称
HYPERDX_API_ENDPOINThttps://in-otel.hyperdx.ioHyperDX 端点

<a id="26-xi-tong-jian-kong-yu-jian-kang-jian-cha"></a>

---

系统监控与健康检查

本文档详细介绍了 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

健康检查端点

系统提供基础的健康检查端点,用于外部监控系统(如负载均衡器、容器编排平台)快速验证服务可用性。

端点详情

端点方法认证要求响应格式
/healthGET{"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. 1. PostgreSQL 数据库:执行 SELECT 1 验证连接
  2. 2. Redis 缓存:执行 PING 命令验证连接
  3. 3. OSS 对象存储:检查配置是否完整
  4. 4. Mem0 记忆服务:验证记忆模块初始化状态
  5. 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_distributionAgent 路由分布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. 1. 运行任务注册:Agent 开始执行时注册运行信息到 Redis
  2. 2. 心跳保活:定期刷新 Redis 键的 TTL,防止任务过期
  3. 3. 工具调用追踪:记录每个工具调用的开始、结束时间和结果
  4. 4. 状态快照:保存运行时的会话状态、对话状态和工具上下文

监控数据结构

Sources: live_runs.py

实时监控 API

端点方法说明
/api/admin/traces/live-runsGET获取当前活跃的运行列表
/api/admin/traces/live-runs/{run_id}GET获取单个运行的详细信息

实时监控 API 支持按 session_id 过滤,返回运行任务的详细状态,包括:

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. 1. 会话列表查询:支持按用户 ID、会话 ID、Agent ID、团队 ID 和状态过滤
  2. 2. 会话详情查询:获取单个会话的所有运行记录摘要
  3. 3. 运行详情查询:获取单次运行的完整 span 树,包括 LLM 调用、工具调用和 Agent 路由详情

Sources: traces.py

追踪查询 API

端点方法说明过滤参数
/api/admin/traces/sessionsGET分页列出会话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 树结构,包括:

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_KEYHyperDX API 密钥空(禁用集成)
OTEL_SERVICE_NAME服务名称空(禁用集成)
OTEL_EXPORTER_OTLP_ENDPOINTOTLP 端点https://in-otel.hyperdx.io
OTEL_SERVICE_VERSION服务版本

日志导出配置

Sources: observability.py

Dashboard 监控面板

Dashboard 提供可视化的监控界面,集成所有监控数据源,提供实时系统状态概览。

监控面板功能

Dashboard 监控面板包含以下组件:

  1. 1. 服务状态卡片:显示所有核心服务的健康状态
  2. 2. 今日统计概览:请求总数、平均响应时间、工具调用种类
  3. 3. 工具调用分布图:Top 10 工具调用的柱状图
  4. 4. Agent 路由分布图:Agent 调用的饼图分布
  5. 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. 1. 存活探针(Liveness Probe):使用 /health 端点
  1. 2. 就绪探针(Readiness Probe):使用 /api/admin/monitor/status 端点

告警配置建议

基于监控数据,建议配置以下告警规则:

告警类型触发条件告警级别
服务不可用任一核心服务状态检查失败严重
响应时间过长平均响应时间 > 10 秒警告
错误率过高最近 5 分钟错误日志 > 10 条严重
内存使用过高进程内存使用 > 80%警告

日志监控策略

  1. 1. 日志级别:生产环境建议使用 INFO 级别,开发环境使用 DEBUG 级别
  2. 2. 日志轮转:配置日志文件轮转,避免磁盘空间不足
  3. 3. 日志聚合:通过 HyperDX 集中收集和分析日志
  4. 4. 敏感信息:确保日志中不包含用户敏感信息

故障排查指南

常见问题及解决方案

问题现象可能原因排查步骤
健康检查失败服务未启动检查服务进程状态,查看启动日志
监控数据不更新Redis 连接问题检查 Redis 连接配置和网络连通性
追踪数据缺失数据库连接问题检查 PostgreSQL 连接和 agno schema
日志导出失败HyperDX 配置错误验证 API 密钥和端点配置

监控数据验证

定期验证监控数据的完整性:

  1. 1. 健康检查验证:确认所有服务状态检查通过
  2. 2. 统计数据验证:对比数据库实际记录与统计结果
  3. 3. 日志完整性:检查错误日志是否正常记录和导出
  4. 4. 追踪数据:验证追踪记录的完整性和准确性

相关文档

<a id="27-dan-yuan-ce-shi-yu-ji-cheng-ce-shi"></a>

---

单元测试与集成测试

本文档系统介绍 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 模型:


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.pyAgent 配置、运行时上下文
记忆与洞察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

下一步阅读

完成本篇后,建议继续阅读以下文档以深入了解相关主题:

<a id="28-zi-dong-hua-ce-shi-yu-xiu-fu"></a>

---

自动化测试与修复

本文档介绍 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 的统一模型。每个规则包含以下关键字段:

字段类型说明
namestr规则名称,建议使用 snake_case
targetLiteral断言作用对象:executionmessagetool_call
selectordict从执行结果中筛选节点的条件
assertionslist[AutoTestAssertion]断言列表,至少包含一个
severityLiteral失败严重级别:errorwarninginfo
description`strNone`规则描述,用于报告展示

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. 1. 选择节点_select_items):根据 targetselector 从执行产物中筛选待检查的节点
  2. 2. 匹配选择器_matches_selector):支持精确匹配和列表匹配
  3. 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

翻译提示词定义了详细的转换规则,例如:

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. 1. 构造执行上下文:解析元数据中的 session_idchanneluser_id
  2. 2. 逐条消息执行:对每条用户消息调用 run_inbound_agent_turn
  3. 3. 收集执行产物:从数据库加载 session runs,提取 messages 和 tool_calls
  4. 4. 错误检测:识别 _NO_CTX_RESULT 和工具错误

关键设计点:

Sources: app/auto_test/outfit_agent/executor.py

Session Inspector

Session Inspector 负责从数据库加载和解析执行数据,核心功能包括:

方法功能
load_session_runsagno.agno_sessions 加载 runs 数据
build_run_messages解析 run 中的 messages 为 AutoTestMessage 列表
build_run_tool_calls提取工具调用为 AutoTestToolCall 列表
extract_run_reply提取 assistant 的最终回复

数据来源为 PostgreSQL 的 agno schema,关键表结构:

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"}

}

响应体包含完整的执行结果,包括 passedrule_resultsartifact 等字段。

Sources: app/routers/admin/auto_tests.py

测试场景配置

场景文件结构

测试场景定义在 tests/auto_repair/scenarios.toml 中,采用 TOML 格式,每个场景包含以下字段:

字段类型说明
idstr场景唯一标识,如 A-1I-2
namestr场景名称
categorystr场景分类
prioritystr优先级:P0P1P2
messageslist[str]用户消息列表,支持多轮对话
expected_toolslist[str]期望调用的工具列表
expected_keywordslist[str]期望回复包含的关键词
notesstr备注说明

场景分类

测试场景按业务功能分为以下类别:

类别说明示例场景
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 LookOOTD 参考图找相似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. 1. 从 scenarios.toml 加载场景定义
  2. 2. 调用 build_legacy_rulesexpected_toolsexpected_keywords 转换为规则
  3. 3. 使用 AutoTestService 执行测试
  4. 4. 生成结果报告并保存到 tests/auto_repair/results/ 目录

Sources: tests/auto_repair/runner.py

方式二:使用 Admin API

通过 Admin API 可以在 Dashboard 中直接执行测试:

  1. 1. 规则翻译:将自然语言规则转换为结构化规则
  2. 2. 测试执行:构造测试用例并执行
  3. 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_callsdata/prompts/agents/*.txt
工具参数错误tool_calls 中 error 字段app/tools/*.py 参数处理
service 层 Bugtool 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. 1. 最小化改动:只修复根因,避免引入新问题
  2. 2. 遵循 AGENTS.md 规约:async IO、禁止硬编码文本、错误信息用英文
  3. 3. 补充单元测试:修复后必须新增测试用例覆盖正常路径和边界情况
  4. 4. 回归验证:修复后重跑失败场景,确保问题已解决

Sources: docs/auto_repair.md

最佳实践

规则编写建议

  1. 1. 优先使用结构化断言:避免依赖 LLM 进行语义判断
  2. 2. 明确选择器条件:使用精确的 selector 筛选目标节点
  3. 3. 合理设置严重级别:核心功能用 error,边缘场景用 warning
  4. 4. 添加规则描述:便于理解规则意图和调试失败原因

测试场景设计

  1. 1. 覆盖核心路径:确保 P0 场景覆盖所有核心功能
  2. 2. 考虑边界情况:无头像、无衣橱、quota 耗尽等场景
  3. 3. 支持多轮对话:使用 messages 列表模拟真实对话流程
  4. 4. 合理设置期望expected_toolsexpected_keywords 不宜过于严格

执行策略

  1. 1. CI/CD 集成:将 P0 场景集成到发布流水线
  2. 2. 定期回归:每周运行完整测试套件
  3. 3. 失败分析:及时分析失败用例,区分环境问题和代码问题
  4. 4. 持续优化:根据测试结果调整规则和场景

常见问题

Q1: 测试执行时出现 _NO_CTX_RESULT 错误

原因:ToolContext 未正确注入,通常是测试方式不正确导致。

解决方案

Q2: 规则翻译结果不准确

原因:自然语言描述过于模糊或复杂。

解决方案

Q3: 测试结果不稳定

原因:Agent 回复的非确定性导致。

解决方案

相关文档

<a id="29-bu-shu-yu-fa-bu"></a>

---

部署与发布

本文档详述 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>= 18Dashboard 前端构建(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.tomluv.lock,自动创建虚拟环境并安装所有依赖。--dev 参数包含 pytest、ruff 等开发工具。如果仅部署生产环境,可省略 --dev

Sources: pyproject.toml, docs/deployment.md

3. 环境变量配置

.env.example 复制为 .env 并填入实际配置:


cp .env.example .env

环境变量分为以下几大类:

类别关键变量说明
LLMOPENAI_API_KEY, OPENAI_BASE_URLLLM 服务密钥与网关地址,推荐统一走 OpenAI 兼容网关
AgentAGENT_NAMEAgent 实例名称
服务HOST, PORT, LOG_LEVELuvicorn 绑定地址与日志级别
企业微信WECHAT_TOKEN, WECHAT_SECRET, WECHAT_CORP_ID企业微信客服与小程序集成凭证
阿里云ALIYUN_AK_ID, ALIYUN_AK_SECRET内容安全审核服务凭证
天气QWEATHER_API_HOST, QWEATHER_KEY_ID和风天气 API 配置
记忆MEM0_ENABLED, MEM0_COLLECTION_NAMEMem0 长期记忆组件开关
监控HYPERDX_API_KEY, OTEL_SERVICE_NAMEHyperDX 可观测性配置
AdminADMIN_SECRET_KEYAdmin Dashboard 访问令牌,生产环境必须设置

.env 文件已被 .gitignore 排除,切勿提交包含密钥的文件到版本控制。数据库 URL(DATABASE_URL)和 Redis URL(REDIS_URL)在 outfit_biz 侧统一管理。

Sources: .env.example

4. 数据库初始化

项目使用 PostgreSQL 存储会话、追踪和业务数据。启动时应用会自动执行轻量级的 Schema 确保操作(非正式迁移),包括创建缺失的列、索引和数据迁移。核心确保逻辑位于 _mem0_lifespan 中,涵盖以下表结构:

如果没有正式的数据库迁移工具,这些 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",

    },

  ],

};

关键配置说明:

参数说明
nameoutfit-agentPM2 进程名称,用于 pm2 status / pm2 logs 识别
scriptuv使用 uv 作为进程启动器,自动处理虚拟环境
argsrun uvicorn app.main:app ...uvicorn 启动参数,绑定 127.0.0.1:8897
cwd/root/outfit/outfit_agent工作目录,必须为项目根目录
interpreternone不使用 Node.js 解释器,直接执行 uv 命令
env_file.env自动加载环境变量文件
autorestarttrue进程崩溃后自动重启
max_memory_restart1G内存超过 1GB 自动重启,防止内存泄漏
watchfalse生产环境禁用文件监听,避免意外重启

启动服务:


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: 应用就绪

启动过程中有三个后台队列处理器被启动:

此外,recover_interrupted_workflows() 会在启动时恢复中断的穿搭工作流会话,确保异常重启不丢失任务状态。

Sources: app/main.py, app/config.py

健康检查

服务提供标准 HTTP 健康检查端点:


GET /health

返回 {"status": "ok"},可用于负载均衡器、容器编排平台或外部监控系统探测服务存活状态。

Sources: app/main.py

可观测性

OpenTelemetry + HyperDX

项目集成了 OpenTelemetry 日志导出,默认对接 HyperDX 平台。配置以下环境变量即可启用:

日志通过 OTLP HTTP 协议导出到 https://in-otel.hyperdx.io(可通过 HYPERDX_API_ENDPOINTOTEL_EXPORTER_OTLP_ENDPOINT 自定义)。此外还支持 Agno 框架级的 DB Span 追踪,追踪数据写入 PostgreSQL 的 agno schema。

Sources: app/observability.py, app/agentos.py

AgentOS 仪表板

AgentOS 提供了内置的管理能力,包括:

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.teamagent.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.pyCanvas WebSocket CLICanvas 功能调试工具

使用示例:


# 确保灰度配置表

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-workers3002Firecrawl 主服务(host 网络)
playwright-service3000无头浏览器(host 网络)
rabbitmq5672任务队列
Redis6379使用已有服务
PostgreSQL5433独立数据库 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_secondsllm_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_TOKENDEV_USER_ID 仅用于开发环境,生产环境务必留空。Sources: .env.example

相关文档

部署完成后,建议继续阅读以下文档以全面了解系统:

<a id="30-xin-zeng-ye-wu-gong-ju"></a>

---

新增业务工具

本文档面向希望为 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_idlang 等)不会暴露给 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]

关键要点

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. 1. 导入工具函数:从新模块导入工具函数
  2. 2. 导入模块工具列表:导入 TOOLS 列表
  3. 3. 添加到聚合列表:将模块工具添加到 ALL_BIZ_TOOLS
  4. 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()}

关键点

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"})

可用的依赖注入上下文

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})

国际化文件位置

工具测试与调试

单元测试

为工具函数编写单元测试,验证功能正确性和边界情况:


# 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. 1. 检查 data/tools/descriptions.toml 中是否有对应工具描述
  2. 2. 确认描述格式正确,没有语法错误
  3. 3. 重启应用或清除工具描述缓存

# 清除缓存

from app.tools.factory import clear_tool_description_cache

clear_tool_description_cache()

工具门控拒绝

问题:工具调用返回 "tool_not_allowed" 错误。

解决方案

  1. 1. 检查工具是否在技能或主团队配置中声明
  2. 2. 确认当前会话的 active_skill 是否包含该工具
  3. 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. 1. 检查数据库配置是否正确
  2. 2. 确认依赖注入上下文是否正确使用
  3. 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. 1. 安全性优先:永远不要在工具函数签名中暴露 user_id 等敏感参数
  2. 2. 描述准确:工具描述要清晰、完整,帮助 LLM 正确使用
  3. 3. 错误处理:返回有意义的错误信息,而不是抛出异常
  4. 4. 职责单一:每个工具只做一件事,保持工具粒度合理
  5. 5. 测试覆盖:编写单元测试和集成测试,确保工具可靠性
  6. 6. 文档同步:及时更新工具描述和相关文档

下一步

完成工具开发后,建议阅读以下文档了解更深入的集成和使用:

<a id="31-xin-zeng-ji-neng-mo-kuai"></a>

---

新增技能模块

本指南详细说明如何为 Outfit Agent 系统新增一个完整的技能(Skill)模块。技能是系统的核心能力单元,每个技能封装了特定的业务场景、指令模板和工具集合,通过动态加载机制与主 Agent 协同工作。

技能架构概览

Outfit Agent 的技能系统采用声明式配置 + 动态加载架构。技能模块由三个核心部分组成:定义文件(skill.toml)、指令模板(data/prompts/skills/)和工具集合。系统通过 SkillRegistry 自动发现并加载所有技能,运行时根据对话状态动态激活。


graph TB

    subgraph "技能定义层"

        A[data/skills/&lt;skill-id&gt;/skill.toml] --> B[技能元数据]

        A --> C[工具声明]

        A --> D[激活模式]

    end

    

    subgraph "指令模板层"

        E[data/prompts/skills/&lt;skill&gt;.zh.txt] --> F[中文指令]

        G[data/prompts/skills/&lt;skill&gt;.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"

关键字段说明

字段类型必需说明
idstring技能唯一标识符,用于运行时引用
namestring技能显示名称,默认使用 id
descriptionstring技能描述,Agent 用于理解技能用途
activationstring激活模式:"on_demand""always"
toolslist技能可用的工具名称列表
allowed_toolslist工具边界声明,用于门控机制
channelsdict渠道特定工具覆盖配置
metadatadict技能元数据,用于分类和管理
instructionsdict指令模板路径配置

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. 1. Agent 识别到需要特定技能的场景
  2. 2. 调用 get_skill_instructions(skill_name="my-new-skill")
  3. 3. 系统加载技能指令并激活技能状态
  4. 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. 1. 工具是否在 ALWAYS_ALLOWED_TOOLS 中(如 get_skill_instructions
  2. 2. 工具是否有技能所有权声明
  3. 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. 1. 基础工具列表:tools 字段定义的工具
  2. 2. 渠道特定工具:channels.<channel_name>.tools 定义的工具
  3. 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. 1. get_skill_reference:读取技能的参考资料

```python get_skill_reference(skill_name="my-new-skill", reference_name="style_guide.txt") ```

  1. 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,

)

Sources: loader.py, loader.py

测试与验证

单元测试

创建测试文件验证技能功能:


# 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. 版本管理

Sources: loader.py, access_tools.py, factory.py

故障排除

常见问题

  1. 1. 技能未加载
  1. 2. 工具调用失败
  1. 3. 指令模板未加载
  1. 4. 渠道特定工具不生效

调试技巧

  1. 1. 启用调试日志

```python import logging logging.getLogger('app.skills').setLevel(logging.DEBUG) ```

  1. 2. 检查技能注册状态

```python from app.skills import get_skill_registry registry = get_skill_registry() print(f"已加载技能: {list(registry.skills.keys())}") ```

  1. 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>

---

提示词管理与国际化

本文档详细介绍了 outfit_agent 项目中的提示词(Prompt)管理系统和国际化(i18n)架构。该系统采用分层设计,支持多语言、多渠道的提示词管理,并通过管理后台实现动态配置与版本控制。

系统架构概述

outfit_agent 的提示词管理系统采用三层架构设计,实现了业务逻辑与内容的分离:

  1. 1. 数据层:存储所有提示词文本和国际化字符串
  2. 2. 服务层:提供统一的加载、缓存和查询接口
  3. 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

核心设计原则

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/

    └── ...

文件命名规范

Sources: 目录结构, prompts目录

国际化(i18n)系统

核心组件

国际化系统由 I18nLoader 类提供核心功能,支持两种加载模式:

  1. 1. 短字符串模式:从 JSON 文件加载键值对
  2. 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. 1. agent 数据目录(最高优先级)
  2. 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)

Sources: i18n.py, i18n.py

语言解析规则

对于每个资源文件,系统按以下顺序查找:

  1. 1. 语言特定文件<stem>.<lang><ext>(如 main_team.en.txt
  2. 2. 基础文件<stem><ext>(如 main_team.txt

示例

  1. 1. prompts/agents/main_team.en.txt
  2. 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

支持的占位符

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. 1. 提示词覆盖:修改 Agent 提示词内容
  2. 2. JSON 覆盖:合并或替换 JSON 配置
  3. 3. 文本覆盖:修改短字符串内容
  4. 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/listGET列出所有资源文件
/api/admin/prompts/contentGET获取资源内容
/api/admin/prompts/contentPUT更新资源内容
/api/admin/prompts/clear-cachePOST清除缓存
/api/admin/prompts/historyGET获取版本历史
/api/admin/prompts/rollbackPOST回滚到指定版本

资源发现机制


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. 1. 确定位置:根据功能选择 data/prompts/ 下的子目录
  2. 2. 创建文件:遵循命名规范 <name>.<lang>.txt
  3. 3. 添加回退:创建无语言后缀的基础文件
  4. 4. 注册配置:在相应的 TOML 配置中引用
  5. 5. 测试验证:确保多语言回退正常工作

国际化字符串管理

  1. 1. JSON 结构:使用扁平键值对,支持点分隔命名空间
  2. 2. 语言覆盖:优先使用语言特定文件
  3. 3. 默认值:确保所有键都有合理的默认值
  4. 4. 缓存管理:修改后清除缓存或重启服务

性能优化建议

  1. 1. 缓存利用:充分利用内存缓存减少 I/O
  2. 2. 懒加载:按需加载大型提示词文件
  3. 3. 批量操作:管理后台支持批量更新
  4. 4. 监控告警:监控缓存命中率和加载延迟

故障排除

常见问题

问题可能原因解决方案
提示词未更新缓存未清除调用 clear-cache API
语言回退失败文件命名错误检查文件命名规范
渠道提示词不生效路径配置错误验证渠道目录结构
灰度配置不生效规则匹配失败检查用户 ID 和规则条件

调试工具

  1. 1. 管理后台:实时查看和编辑提示词
  2. 2. 日志记录:详细的加载和缓存日志
  3. 3. 单元测试:覆盖各种边界情况
  4. 4. 性能监控:缓存命中率和响应时间

相关文档