从 0 到 1 搭建你的 AI Agent 平台:当 Agent 有了工厂,人人都能造同事

0 阅读22分钟

从 0 到 1 搭建你的 AI Agent 平台:当 Agent 有了工厂,人人都能造同事

前端 AI Skill 体系 · 第 7 篇(Agent 平台篇)

上一篇,我们用 100 行代码造了一个 Research Agent,又用 200 行搭了一个 PM + Coder + Reviewer 三人团队。

跑通之后,我兴奋了大概两天。

第三天,问题来了。

同事问我:"你这个 Agent 挺好用,能不能帮我也搞一个?"我说行,然后花了一下午帮他改 agent.py 里的 instructions、换工具、调参数。

第四天,另一个同事也来了。第五天,产品经理说:"能不能让非技术人员自己配?"

我盯着终端里那个 python main.py,突然意识到一件事:

我造了 3 个好同事,但我没有工厂。

每来一个需求,我就得手写一遍。改一个 Agent 的指令,就得打开编辑器、改代码、重启。加一个工具,就得写一个 @function_tool、注册、测试。

这不是平台。这是手工作坊。

今天这篇,我们把它变成工厂。


一、为什么需要 Agent 平台(vs 手写脚本)

手写脚本的天花板

上一篇的 3 人团队,代码量不大,但维护成本不低:

操作手写脚本平台化
新建一个 Agentagent.py,定义 instructions、绑定 tools填表单,选模型,拖工具
修改 Agent 指令改代码 → 重启进程改配置 → 热更新
换一个模型model="qwen-plus" → 重启下拉框切换
加一个工具@function_tool → 注册 → 重启上传 / 从市场安装
给非技术人员用❌ 不可能✅ Web UI
多 Agent 协作编排手写 Handoff 代码可视化拖拽 / YAML
查看运行历史print 日志结构化 Tracing 面板

手写脚本适合"我自己用"。平台适合"别人也能用"。

什么时候该做平台?

不是所有人都需要。我的判断标准:

信号说明
第 3 个人来要 Agent你开始变成"Agent 客服"
每周改 3 次以上 instructions配置和代码该分离了
需要非技术人员操作终端不是所有人的朋友
Agent 数量 > 5手写 Handoff 开始混乱
需要审计/回溯"上周那个 Agent 为什么删了我的文件?"

如果你只有 1-2 个 Agent、自己用、不怎么改——别做平台。 手写脚本够了。

但如果你命中了 3 条以上,继续往下看。

我们要造什么?

一句话:一个 Web 应用,让任何人都能通过浏览器创建、配置、编排、运行 AI Agent。

核心能力:

┌─────────────────────────────────────────────────────┐
                  Agent Platform                      
                                                      
  🧠 模型管理    多模型接入/切换/成本监控              
  📋 Skill 管理  上传/启用/禁用/版本控制              
  🤖 Agent 配置  角色/指令/工具绑定/模型选择          
  🔀 Agent 编排  可视化拖拽 or YAML 定义协作流程      
  💾 用户记忆    短期/长期/向量检索/跨会话持久化      
  🔌 MCP 插件    工具市场/第三方集成/自定义工具       
                                                      
  前端:React + Next.js(Web UI)                     
  后端:Python FastAPI(Agent 运行时)                
  模型:OpenAI / 百炼 / Ollama(可切换)              
└─────────────────────────────────────────────────────┘

二、架构设计

技术选型:为什么是 React + Python

先说结论,再说为什么。

选型为什么
前端React + Next.js + Tailwind CSS前端同学最熟、SSR 首屏快、组件生态丰富
后端Python FastAPIAgent 生态全在 Python(OpenAI Agents SDK / LangChain / CrewAI)
数据库SQLite → PostgreSQL个人用 SQLite 够了,多人用再切 PG
向量库ChromaDB(本地)→ Qdrant(生产)记忆检索用,本地零依赖
模型层LiteLLM 统一代理一套代码接 100+ 模型
部署Docker Compose一键起前端 + 后端 + 数据库

为什么不用 Node.js 做后端?

因为 Agent 运行时在 Python。OpenAI Agents SDK、LangChain、CrewAI、AutoGen——全是 Python。用 Node 做后端意味着你得通过 HTTP 调 Python 服务,多一层网络开销和调试成本。

为什么不用 Next.js 全栈?

Next.js 的 API Routes 跑 Python Agent 不现实。前后端分离,各司其职:Next.js 管 UI,FastAPI 管 Agent 运行时。

整体架构图

┌─────────────────────────────────────────────────────────────┐
│                        用户浏览器                            │
│  ┌───────────────────────────────────────────────────────┐  │
│  │              Next.js (React + Tailwind)                │  │
│  │                                                       │  │
│  │  /agents     Agent 列表 + 创建/编辑                    │  │
│  │  /agents/:id Agent 详情 + 对话测试                     │  │
│  │  /skills     Skill 管理(上传/启用/禁用)              │  │
│  │  /models     模型管理(接入/切换/测试)                │  │
│  │  /workflows  编排画布(拖拽 or YAML)                  │  │
│  │  /memory     记忆管理(查看/搜索/清理)                │  │
│  │  /plugins    MCP 插件市场                              │  │
│  │  /settings   全局设置                                  │  │
│  └───────────────────────┬───────────────────────────────┘  │
└──────────────────────────┼──────────────────────────────────┘
                           │ REST / WebSocket
┌──────────────────────────┼──────────────────────────────────┐
│                    FastAPI (Python)                          │
│                          │                                   │
│  ┌──────────┐  ┌────────┴───┐  ┌──────────┐  ┌──────────┐ │
│  │ Agent    │  │ Workflow   │  │ Memory   │  │ Plugin   │ │
│  │ Service  │  │ Engine     │  │ Service  │  │ Registry │ │
│  │          │  │            │  │          │  │          │ │
│  │ CRUD     │  │ Handoff    │  │ 短期记忆 │  │ MCP 协议 │ │
│  │ 运行     │  │ 编排       │  │ 长期记忆 │  │ 工具市场 │ │
│  │ Tracing  │  │ 重试       │  │ 向量检索 │  │ 自定义   │ │
│  └────┬─────┘  └────────────┘  └────┬─────┘  └──────────┘ │
│       │                              │                       │
│  ┌────┴──────────────────────────────┴─────┐                │
│  │           LiteLLM 统一模型代理           │                │
│  │  OpenAI / 百炼 / Claude / Ollama / ...  │                │
│  └─────────────────────────────────────────┘                │
│                                                              │
│  ┌──────────┐  ┌──────────┐                                 │
│  │ SQLite/PG│  │ ChromaDB │                                 │
│  │ 配置存储 │  │ 向量存储 │                                 │
│  └──────────┘  └──────────┘                                 │
└──────────────────────────────────────────────────────────────┘

项目目录结构

agent-platform/
│
├── frontend/                    # Next.js 前端
│   ├── app/
│   │   ├── agents/              # Agent 管理页
│   │   │   ├── page.tsx         #   列表
│   │   │   ├── new/page.tsx     #   创建
│   │   │   └── [id]/page.tsx    #   详情 + 对话
│   │   ├── skills/page.tsx      # Skill 管理
│   │   ├── models/page.tsx      # 模型管理
│   │   ├── workflows/page.tsx   # 编排画布
│   │   ├── memory/page.tsx      # 记忆管理
│   │   ├── plugins/page.tsx     # MCP 插件
│   │   └── layout.tsx           # 全局布局(侧边栏导航)
│   ├── components/              # 通用组件
│   │   ├── AgentCard.tsx
│   │   ├── ChatPanel.tsx
│   │   ├── WorkflowCanvas.tsx
│   │   └── ModelSelector.tsx
│   ├── lib/
│   │   └── api.ts               # API 客户端
│   ├── package.json
│   └── tailwind.config.ts
│
├── backend/                     # FastAPI 后端
│   ├── app/
│   │   ├── main.py              # FastAPI 入口
│   │   ├── routers/
│   │   │   ├── agents.py        # Agent CRUD + 运行
│   │   │   ├── skills.py        # Skill 管理
│   │   │   ├── models.py        # 模型管理
│   │   │   ├── workflows.py     # 编排引擎
│   │   │   ├── memory.py        # 记忆服务
│   │   │   └── plugins.py       # MCP 插件
│   │   ├── services/
│   │   │   ├── agent_runner.py  # Agent 运行时(核心)
│   │   │   ├── workflow_engine.py
│   │   │   ├── memory_store.py
│   │   │   └── model_proxy.py   # LiteLLM 封装
│   │   ├── models/              # 数据模型(Pydantic)
│   │   │   ├── agent.py
│   │   │   ├── skill.py
│   │   │   └── workflow.py
│   │   └── db/
│   │       ├── database.py      # SQLite/PG 连接
│   │       └── migrations/
│   ├── requirements.txt
│   └── Dockerfile
│
├── docker-compose.yml           # 一键部署
├── .env.example
└── README.md

数据模型:Agent 长什么样

在写任何 UI 之前,先定义清楚"一个 Agent 在数据库里是什么":

# backend/app/models/agent.py
from pydantic import BaseModel
from typing import Optional
from enum import Enum


class AgentStatus(str, Enum):
    DRAFT = "draft"
    ACTIVE = "active"
    ARCHIVED = "archived"


class AgentConfig(BaseModel):
    """Agent 的完整配置——这就是平台的核心数据结构"""

    # 基本信息
    id: str
    name: str                          # "Research Assistant"
    description: str                   # "能搜索、能写文件的调研助手"
    avatar: Optional[str] = None       # 头像 URL
    status: AgentStatus = AgentStatus.DRAFT

    # 模型配置
    model: str = "qwen-plus"           # 模型标识
    temperature: float = 0.7
    max_tokens: int = 4096

    # 核心:指令(就是上一篇的 instructions)
    instructions: str = ""

    # 工具绑定
    tools: list[str] = []              # ["web_search", "read_file", "write_file"]

    # Skill 绑定(Agent 可以加载多个 Skill 作为知识补充)
    skills: list[str] = []             # ["code-review-rules", "writing-style"]

    # 运行参数
    max_turns: int = 10                # 最大工具调用轮次
    needs_approval_tools: list[str] = []  # 需要人工确认的工具

    # 编排(如果是多 Agent 协作)
    handoffs: list[str] = []           # 可以交接给哪些 Agent ID

    # 记忆配置
    memory_enabled: bool = True
    memory_type: str = "sqlite"        # sqlite / redis / vector

    # 元数据
    created_at: str
    updated_at: str
    created_by: str = "admin"

对比上一篇:上一篇的 Agent 定义是硬编码在 agent.py 里的。现在,它变成了数据库里的一条记录。创建 Agent = INSERT,修改指令 = UPDATE,运行 Agent = SELECT + 构建 Agent() 对象。

这就是"平台化"的本质:把代码里的配置变成数据。


三、核心模块搭建

6 个模块,按依赖顺序搭:模型管理 → Skill 管理 → Agent 配置 → Agent 编排 → 用户记忆 → MCP 插件。

3.1 模型管理:多模型接入/切换

问题:上一篇我们硬编码了 model="qwen-plus"。如果用户想用 GPT-4o、Claude、本地 Ollama 呢?

方案:用 LiteLLM 做统一代理,前端只管选模型名。

# backend/app/services/model_proxy.py
from litellm import acompletion
from typing import Optional


# 平台支持的模型注册表
MODEL_REGISTRY = {
    # 百炼/通义千问(国内直连)
    "qwen-plus": {
        "provider": "dashscope",
        "display_name": "通义千问 Plus",
        "max_tokens": 131072,
        "cost_per_1k_input": 0.004,   # 元
        "cost_per_1k_output": 0.012,
    },
    "qwen-turbo": {
        "provider": "dashscope",
        "display_name": "通义千问 Turbo",
        "max_tokens": 131072,
        "cost_per_1k_input": 0.002,
        "cost_per_1k_output": 0.006,
    },
    # OpenAI
    "gpt-4o": {
        "provider": "openai",
        "display_name": "GPT-4o",
        "max_tokens": 128000,
        "cost_per_1k_input": 0.0175,
        "cost_per_1k_output": 0.07,
    },
    "gpt-4o-mini": {
        "provider": "openai",
        "display_name": "GPT-4o Mini",
        "max_tokens": 128000,
        "cost_per_1k_input": 0.001,
        "cost_per_1k_output": 0.004,
    },
    # 本地 Ollama
    "ollama/llama3.1:8b": {
        "provider": "ollama",
        "display_name": "Llama 3.1 8B(本地)",
        "max_tokens": 8192,
        "cost_per_1k_input": 0,
        "cost_per_1k_output": 0,
    },
}


async def chat_completion(
    model: str,
    messages: list[dict],
    temperature: float = 0.7,
    max_tokens: int = 4096,
) -> str:
    """统一模型调用入口——不管底层是百炼、OpenAI 还是 Ollama"""
    response = await acompletion(
        model=model,
        messages=messages,
        temperature=temperature,
        max_tokens=max_tokens,
    )
    return response.choices[0].message.content


async def test_model_connection(model: str) -> dict:
    """测试模型连通性——前端"测试连接"按钮调用"""
    try:
        result = await chat_completion(
            model=model,
            messages=[{"role": "user", "content": "回复'连接成功'四个字"}],
            max_tokens=20,
        )
        return {"status": "ok", "response": result}
    except Exception as error:
        return {"status": "error", "message": str(error)}
# backend/app/routers/models.py
from fastapi import APIRouter
from app.services.model_proxy import MODEL_REGISTRY, test_model_connection

router = APIRouter(prefix="/api/models", tags=["models"])


@router.get("/")
async def list_models():
    """返回所有可用模型——前端下拉框的数据源"""
    return [
        {"id": model_id, **config}
        for model_id, config in MODEL_REGISTRY.items()
    ]


@router.post("/{model_id}/test")
async def test_connection(model_id: str):
    """测试模型连通性"""
    if model_id not in MODEL_REGISTRY:
        return {"status": "error", "message": f"未知模型:{model_id}"}
    return await test_model_connection(model_id)

前端交互:模型管理页就是一个卡片列表,每张卡片显示模型名、提供商、价格、连通状态。点"测试连接"实时验证。创建 Agent 时,模型选择就是一个下拉框。

3.2 Skill 管理:上传/启用/禁用

问题:上一篇的 Skill 是放在项目目录里的 .md 文件。平台化之后,Skill 变成数据库记录,支持动态加载。

方案:Skill = 一段 Markdown 指令 + 元数据。Agent 运行时,把绑定的 Skill 内容拼接到 instructions 里。

# backend/app/models/skill.py
from pydantic import BaseModel
from typing import Optional


class SkillConfig(BaseModel):
    id: str
    name: str                          # "代码审查规范"
    description: str                   # "定义代码审查的维度和标准"
    content: str                       # Markdown 正文(就是 SKILL.md 的内容)
    version: str = "1.0.0"
    enabled: bool = True
    tags: list[str] = []               # ["code-review", "frontend"]
    created_at: str
    updated_at: str
# backend/app/routers/skills.py
from fastapi import APIRouter, UploadFile, File
from app.db.database import get_db

router = APIRouter(prefix="/api/skills", tags=["skills"])


@router.get("/")
async def list_skills(enabled_only: bool = False):
    """列出所有 Skill"""
    db = get_db()
    query = "SELECT * FROM skills"
    if enabled_only:
        query += " WHERE enabled = 1"
    query += " ORDER BY updated_at DESC"
    return db.execute(query).fetchall()


@router.post("/upload")
async def upload_skill(file: UploadFile = File(...)):
    """上传 .md 文件创建 Skill——兼容上一篇的 SKILL.md 格式"""
    content = (await file.read()).decode("utf-8")

    # 从 Markdown 中提取 name 和 description
    name = file.filename.replace(".md", "")
    description = ""
    for line in content.split("\n"):
        if line.startswith("# "):
            name = line[2:].strip()
        elif line.startswith("> ") and not description:
            description = line[2:].strip()

    db = get_db()
    db.execute(
        "INSERT INTO skills (name, description, content, enabled) VALUES (?, ?, ?, 1)",
        (name, description, content),
    )
    db.commit()
    return {"status": "ok", "name": name}


@router.patch("/{skill_id}/toggle")
async def toggle_skill(skill_id: str):
    """启用/禁用 Skill——不影响已绑定的 Agent,下次运行时生效"""
    db = get_db()
    db.execute("UPDATE skills SET enabled = NOT enabled WHERE id = ?", (skill_id,))
    db.commit()
    return {"status": "ok"}

Skill 和 Agent 的关系:Agent 的 instructions 是"你是谁、怎么工作",Skill 是"你要遵守的规范"。运行时拼接:

# backend/app/services/agent_runner.py(片段)
def build_instructions(agent_config, skills: list[str]) -> str:
    """把 Agent 指令 + 绑定的 Skill 内容拼成完整 instructions"""
    parts = [agent_config.instructions]

    for skill_content in skills:
        parts.append(f"\n\n---\n## 附加规范\n{skill_content}")

    return "\n".join(parts)

3.3 Agent 配置:角色/指令/工具绑定

这是平台的核心页面。用户通过表单创建一个 Agent,而不是写代码。

# backend/app/routers/agents.py
from fastapi import APIRouter
from agents import Agent, Runner, function_tool, set_default_openai_api
from app.models.agent import AgentConfig
from app.services.agent_runner import build_instructions, get_tools_by_names
from app.db.database import get_db

router = APIRouter(prefix="/api/agents", tags=["agents"])


@router.post("/")
async def create_agent(config: AgentConfig):
    """创建 Agent——前端表单提交"""
    db = get_db()
    db.execute(
        """INSERT INTO agents
           (id, name, description, model, instructions, tools, skills,
            max_turns, temperature, status)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
        (
            config.id, config.name, config.description,
            config.model, config.instructions,
            ",".join(config.tools), ",".join(config.skills),
            config.max_turns, config.temperature, "draft",
        ),
    )
    db.commit()
    return {"status": "ok", "id": config.id}


@router.post("/{agent_id}/run")
async def run_agent(agent_id: str, user_input: str):
    """运行 Agent——前端对话面板调用"""
    db = get_db()
    row = db.execute("SELECT * FROM agents WHERE id = ?", (agent_id,)).fetchone()
    if not row:
        return {"error": "Agent 不存在"}

    # 从数据库配置 → 构建 Agent 对象
    skills_content = load_skills(row["skills"])
    instructions = build_instructions(row, skills_content)
    tools = get_tools_by_names(row["tools"].split(","))

    agent = Agent(
        name=row["name"],
        model=row["model"],
        instructions=instructions,
        tools=tools,
    )

    # 百炼需要这两行(和上一篇一样)
    if "qwen" in row["model"]:
        import os
        os.environ["OPENAI_AGENTS_DISABLE_TRACING"] = "true"
        set_default_openai_api("chat_completions")

    result = await Runner.run(
        agent,
        input=user_input,
        max_turns=row["max_turns"],
    )
    return {"output": result.final_output}

前端表单长什么样?

┌─────────────────────────────────────────────────┐
│  创建 Agent                                      │
│                                                  │
│  名称:    [Research Assistant          ]        │
│  描述:    [能搜索、能写文件的调研助手    ]        │
│                                                  │
│  模型:    [通义千问 Plus (qwen-plus)  ▼]        │
│  温度:    [0.7 ──────●────── ]                  │
│                                                  │
│  指令(System Prompt):                         │
│  ┌─────────────────────────────────────────┐    │
│  │ 你是一个研究助理。                       │    │
│  │                                          │    │
│  │ ## 工作流程                              │    │
│  │ 1. 理解研究问题,拆解为子问题            │    │
│  │ 2. 使用 web_search 搜索                  │    │
│  │ ...                                      │    │
│  └─────────────────────────────────────────┘    │
│                                                  │
│  工具:  ☑ web_search  ☑ read_file  ☑ write_file│
│          ☐ code_exec   ☐ git_ops               │
│                                                  │
│  Skill: ☑ 代码审查规范  ☐ 写作风格指南         │
│                                                  │
│  最大轮次:[10]   需确认工具:[delete_file]      │
│                                                  │
│         [ 保存草稿 ]  [ 保存并测试 ]             │
└─────────────────────────────────────────────────┘

关键设计决策:指令编辑用纯文本 textarea,不用富文本编辑器。因为 instructions 本质是给 LLM 看的 Markdown,富文本反而添乱。

3.4 Agent 编排:可视化拖拽 or YAML

问题:上一篇的 Handoff 是硬编码的:

pm_agent.handoffs = [handoff(coder_agent)]
coder_agent.handoffs = [handoff(reviewer_agent)]

平台化之后,编排关系存在数据库里,支持两种编辑方式。

方案 A:YAML 定义(推荐起步)

# workflow: dev-team
name: 开发团队
description: PM  Coder  Reviewer 三人协作
entry_agent: pm-agent-id

agents:
  - id: pm-agent-id
    role: PM
    handoffs: [coder-agent-id]

  - id: coder-agent-id
    role: Coder
    handoffs: [reviewer-agent-id]

  - id: reviewer-agent-id
    role: Reviewer
    handoffs: [coder-agent-id]   # 打回重写

retry:
  max_attempts: 2
  on_failure: human_escalation
# backend/app/services/workflow_engine.py
import yaml
from agents import Agent, Runner, handoff


async def run_workflow(yaml_content: str, user_input: str) -> str:
    """解析 YAML 编排 → 构建 Handoff 链 → 运行"""
    config = yaml.safe_load(yaml_content)

    # 从数据库加载每个 Agent 的配置,构建 Agent 对象
    agent_map = {}
    for agent_def in config["agents"]:
        agent_config = load_agent_from_db(agent_def["id"])
        agent_map[agent_def["id"]] = build_agent(agent_config)

    # 建立 Handoff 关系
    for agent_def in config["agents"]:
        agent = agent_map[agent_def["id"]]
        agent.handoffs = [
            handoff(agent_map[target_id])
            for target_id in agent_def.get("handoffs", [])
        ]

    # 从入口 Agent 开始运行
    entry_agent = agent_map[config["entry_agent"]]
    result = await Runner.run(
        entry_agent,
        input=user_input,
        max_turns=20,
    )
    return result.final_output

方案 B:可视化拖拽(进阶)

React Flow 做拖拽画布:

┌──────────────────────────────────────────────────┐
│  编排画布                                         │
│                                                   │
│  ┌────────┐         ┌────────┐         ┌────────┐│
│  │   PM   │────────▶│ Coder  │────────▶│Reviewer││
│  │ 需求分析│         │ 编码实现│         │ 代码审查││
│  └────────┘         └────────┘         └───┬────┘│
│                            ▲                 │    │
│                            │    打回重写      │    │
│                            └─────────────────┘    │
│                                                   │
│  拖拽节点 = 选 Agent                              │
│  连线 = Handoff 关系                              │
│  双击节点 = 编辑 Agent 配置                       │
│  导出 = 生成 YAML                                 │
└──────────────────────────────────────────────────┘

我的建议:先做 YAML,跑通后再加拖拽。拖拽是 UI 糖,YAML 是骨架。别本末倒置。

3.5 用户记忆:短期/长期/向量检索

问题:上一篇的 Agent 没有记忆。每次 Runner.run() 都是全新对话。用户说"继续上次的调研",Agent 一脸懵。

方案:三层记忆架构。

┌─────────────────────────────────────────────┐
│              记忆架构                        │
│                                              │
│  短期记忆(Working Memory)                  │
│  ├── 当前对话的上下文                        │
│  ├── 存储:内存(Python dict)               │
│  └── 生命周期:单次会话                      │
│                                              │
│  长期记忆(Long-term Memory)                │
│  ├── 跨会话的事实和偏好                      │
│  ├── "用户喜欢中文报告"                      │
│  ├── "用户的项目用 React + TypeScript"       │
│  ├── 存储:SQLite / PostgreSQL               │
│  └── 生命周期:永久(可手动清理)            │
│                                              │
│  语义记忆(Vector Memory)                   │
│  ├── 历史对话的向量化存储                    │
│  ├── 支持"上次我们讨论过什么"类查询          │
│  ├── 存储:ChromaDB(本地)/ Qdrant(生产)  │
│  └── 生命周期:可配置过期策略                │
└─────────────────────────────────────────────┘
# backend/app/services/memory_store.py
import sqlite3
import chromadb
from datetime import datetime


class MemoryStore:
    """Agent 的记忆管理器"""

    def __init__(self, agent_id: str, user_id: str = "default"):
        self.agent_id = agent_id
        self.user_id = user_id

        # 长期记忆 + 对话历史持久化:SQLite
        self.db = sqlite3.connect("memory.db")
        self.db.execute("""
            CREATE TABLE IF NOT EXISTS long_term_memory (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                agent_id TEXT,
                user_id TEXT,
                content TEXT,
                category TEXT DEFAULT 'fact',
                created_at TEXT DEFAULT CURRENT_TIMESTAMP
            )
        """)
        self.db.execute("""
            CREATE TABLE IF NOT EXISTS conversation_history (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                agent_id TEXT,
                user_id TEXT,
                conversation_id TEXT,
                role TEXT,
                content TEXT,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP
            )
        """)

        # 语义记忆:ChromaDB
        self.chroma = chromadb.Client()
        self.collection = self.chroma.get_or_create_collection(
            name=f"agent_{agent_id}",
            metadata={"hnsw:space": "cosine"},
        )

    def save_long_term(self, content: str, category: str = "fact"):
        """保存长期记忆——Agent 运行后自动提取关键信息"""
        self.db.execute(
            "INSERT INTO long_term_memory (agent_id, user_id, content, category) VALUES (?, ?, ?, ?)",
            (self.agent_id, self.user_id, content, category),
        )
        self.db.commit()

    def save_conversation(self, conversation_id: str, messages: list[dict]):
        """保存对话到向量库——支持语义检索"""
        for i, msg in enumerate(messages):
            self.collection.add(
                documents=[msg["content"]],
                ids=[f"{conversation_id}_{i}"],
                metadatas=[{
                    "role": msg["role"],
                    "timestamp": datetime.now().isoformat(),
                }],
            )

    def save_history(self, conversation_id: str, messages: list[dict]):
        """对话历史持久化——重启不丢,支持回看"""
        for msg in messages:
            self.db.execute(
                "INSERT INTO conversation_history (agent_id, user_id, conversation_id, role, content) VALUES (?, ?, ?, ?, ?)",
                (self.agent_id, self.user_id, conversation_id, msg["role"], msg["content"]),
            )
        self.db.commit()

    def load_history(self, conversation_id: str) -> list[dict]:
        """加载某次对话的完整历史"""
        rows = self.db.execute(
            "SELECT role, content FROM conversation_history WHERE conversation_id = ? ORDER BY id",
            (conversation_id,),
        ).fetchall()
        return [{"role": row[0], "content": row[1]} for row in rows]

    def list_conversations(self, limit: int = 20) -> list[dict]:
        """列出最近的对话——用于 UI 侧边栏展示"""
        rows = self.db.execute(
            "SELECT conversation_id, MIN(created_at) as started_at, COUNT(*) as msg_count FROM conversation_history WHERE agent_id = ? GROUP BY conversation_id ORDER BY started_at DESC LIMIT ?",
            (self.agent_id, limit),
        ).fetchall()
        return [{"conversation_id": r[0], "started_at": r[1], "message_count": r[2]} for r in rows]

    def recall(self, query: str, top_k: int = 5) -> list[str]:
        """语义检索——"上次我们讨论过什么""""
        results = self.collection.query(
            query_texts=[query],
            n_results=top_k,
        )
        return results["documents"][0] if results["documents"] else []

    def get_context_for_prompt(self, user_input: str) -> str:
        """构建注入到 instructions 的记忆上下文"""
        # 1. 取长期记忆
        long_term = self.db.execute(
            "SELECT content FROM long_term_memory WHERE agent_id = ? ORDER BY created_at DESC LIMIT 10",
            (self.agent_id,),
        ).fetchall()

        # 2. 语义检索相关历史
        semantic = self.recall(user_input, top_k=3)

        parts = []
        if long_term:
            facts = "\n".join(f"- {row[0]}" for row in long_term)
            parts.append(f"## 用户偏好和事实\n{facts}")
        if semantic:
            history = "\n".join(f"- {doc}" for doc in semantic)
            parts.append(f"## 相关历史对话\n{history}")

        return "\n\n".join(parts) if parts else ""

运行时注入

# agent_runner.py(片段)
async def run_agent_with_memory(agent_config, user_input: str):
    memory = MemoryStore(agent_config.id)

    # 把记忆上下文拼到 instructions 里
    memory_context = memory.get_context_for_prompt(user_input)
    full_instructions = agent_config.instructions
    if memory_context:
        full_instructions += f"\n\n---\n{memory_context}"

    agent = Agent(
        name=agent_config.name,
        model=agent_config.model,
        instructions=full_instructions,
        tools=get_tools_by_names(agent_config.tools),
    )

    result = await Runner.run(agent, input=user_input, max_turns=agent_config.max_turns)

    # 运行后保存对话
    messages = [
        {"role": "user", "content": user_input},
        {"role": "assistant", "content": result.final_output},
    ]
    memory.save_conversation(conversation_id=result.run_id, messages=messages)  # 向量库(语义检索)
    memory.save_history(conversation_id=result.run_id, messages=messages)      # SQLite(持久化回看)

    return result.final_output

3.6 MCP 插件:工具市场

上一篇的工具是硬编码在 tools.py 里的。平台化之后,工具应该像插件一样可安装、可卸载

MCP(Model Context Protocol) 是 Anthropic 提出的开放协议,2026 年已成为 Agent 工具生态的事实标准。平台的插件系统支持三种工具来源:内置工具(开箱即用)、MCP Server(社区生态,即插即用)、自定义工具(用户上传 Python 函数,沙箱执行)。

📦 插件注册表(PluginRegistry)、MCP Server 通信、安装/卸载 API 的完整实现 → 评论区扣「Agent平台」或私信获取


四、Web UI 实现

技术栈:Next.js 15 + React 19 + Tailwind CSS + shadcn/ui

为什么选这套?前端同学最熟、组件生态最丰富、AI 生成代码的质量最高(训练数据里 React 代码最多)。

前端包含 7 个页面(Agent 管理、Skill 管理、模型管理、编排画布、记忆管理、插件市场、全局设置),文章篇幅原因,这里只展示最核心的交互——对话测试面板,其余页面的完整代码在完整版中。

📦 完整代码(含 Web UI 全部页面 + API Key 管理面板)→ 评论区扣「Agent平台」或私信获取

4.1 对话测试面板

创建完 Agent,最重要的交互是立刻测试

// frontend/components/ChatPanel.tsx
"use client"
import { useState, useRef, useEffect } from "react"
import { api } from "@/lib/api"

interface Message {
  role: "user" | "assistant"
  content: string
}

export function ChatPanel({ agentId }: { agentId: string }) {
  const [messages, setMessages] = useState<Message[]>([])
  const [input, setInput] = useState("")
  const [loading, setLoading] = useState(false)
  const bottomRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    bottomRef.current?.scrollIntoView({ behavior: "smooth" })
  }, [messages])

  async function sendMessage() {
    if (!input.trim() || loading) return

    const userMessage = input.trim()
    setInput("")
    setMessages((prev) => [...prev, { role: "user", content: userMessage }])
    setLoading(true)

    try {
      const result = await api.post(`/api/agents/${agentId}/run`, {
        user_input: userMessage,
      })
      setMessages((prev) => [
        ...prev,
        { role: "assistant", content: result.output },
      ])
    } catch (error) {
      setMessages((prev) => [
        ...prev,
        { role: "assistant", content: `❌ 运行出错:${error}` },
      ])
    } finally {
      setLoading(false)
    }
  }

  return (
    <div className="flex h-[600px] flex-col rounded-xl border border-zinc-800 bg-zinc-900">
      {/* 消息列表 */}
      <div className="flex-1 space-y-4 overflow-y-auto p-4">
        {messages.length === 0 && (
          <p className="mt-20 text-center text-sm text-zinc-500">
            发送消息开始测试你的 Agent 👇
          </p>
        )}
        {messages.map((msg, i) => (
          <div
            key={i}
            className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}
          >
            <div
              className={`max-w-[80%] rounded-lg px-4 py-2 text-sm ${
                msg.role === "user"
                  ? "bg-emerald-600 text-white"
                  : "bg-zinc-800 text-zinc-200"
              }`}
            >
              <pre className="whitespace-pre-wrap font-sans">{msg.content}</pre>
            </div>
          </div>
        ))}
        {loading && (
          <div className="flex justify-start">
            <div className="rounded-lg bg-zinc-800 px-4 py-2 text-sm text-zinc-400">
              🤔 思考中...
            </div>
          </div>
        )}
        <div ref={bottomRef} />
      </div>

      {/* 输入框 */}
      <div className="border-t border-zinc-800 p-4">
        <div className="flex gap-2">
          <input
            value={input}
            onChange={(e) => setInput(e.target.value)}
            onKeyDown={(e) => e.key === "Enter" && sendMessage()}
            placeholder="输入消息测试 Agent..."
            className="flex-1 rounded-lg border border-zinc-700 bg-zinc-800 px-4 py-2 text-sm text-zinc-100 placeholder-zinc-500 focus:border-emerald-500 focus:outline-none"
          />
          <button
            onClick={sendMessage}
            disabled={loading}
            className="rounded-lg bg-emerald-600 px-4 py-2 text-sm font-medium hover:bg-emerald-500 disabled:opacity-50"
          >
            发送
          </button>
        </div>
      </div>
    </div>
  )
}

其余页面(全局布局、Agent 列表、API 客户端、Skill 管理、模型管理、编排画布、记忆管理、插件市场、全局设置)的完整代码,篇幅原因不在这里展开。

📦 完整代码(含 Web UI 全部 7 个页面 + API 客户端 + API Key 管理面板)→ 评论区扣「Agent平台」或私信获取


五、部署与商业化思考

5.1 一键部署:Docker Compose

# docker-compose.yml
version: "3.8"

services:
  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    environment:
      - NEXT_PUBLIC_API_URL=http://backend:8000
    depends_on:
      - backend

  backend:
    build: ./backend
    ports:
      - "8000:8000"
    env_file:
      - .env
    volumes:
      - ./data:/app/data        # SQLite + ChromaDB 持久化
    depends_on:
      - ollama

  ollama:
    image: ollama/ollama:latest
    ports:
      - "11434:11434"
    volumes:
      - ollama_data:/root/.ollama

volumes:
  ollama_data:
# 一键启动
docker compose up -d

# 访问
# 前端:http://localhost:3000
# 后端 API:http://localhost:8000/docs(FastAPI 自带 Swagger)
# Ollama:http://localhost:11434

5.2 商业化路径:从个人到企业

阶段用户功能收费
开源版个人开发者单用户、SQLite、本地模型免费
Pro 版小团队(5人)多用户、PostgreSQL、云端模型、Tracing¥99/月
企业版企业SSO、RBAC、审计日志、私有部署、SLA按需报价

开源是获客手段,不是商业模式。 核心壁垒不在代码,在于:

  • 模型调优经验(什么 instructions 效果好)
  • Skill 生态(社区贡献的高质量 Skill)
  • 工具集成深度(MCP 插件数量和质量)

5.3 成本估算

组件个人用团队用
服务器本地 / 免费云2C4G 云服务器 ~¥100/月
模型 API百炼免费额度 / Ollama 本地~¥200-500/月
向量库ChromaDB 本地Qdrant Cloud ~$25/月
域名 + SSL~¥60/年同左
总计¥0¥300-600/月

六、Q&A

Q1: 为什么不直接用 Dify / Coze / FastGPT?

好问题。它们确实能做很多本文提到的事。

维度Dify / Coze自建平台
上手速度快,注册即用慢,要搭环境
定制深度受限于平台能力完全可控
数据隐私数据在别人服务器数据在自己手里
学习价值低(拖拖拽拽)高(理解 Agent 全链路)
成本免费额度有限本地模型零成本

如果你只是想用 Agent,用 Dify。如果你想理解 Agent 怎么工作、想完全控制、想给团队定制——自建。

本文的目的不是"造一个比 Dify 好的产品",是让你理解 Agent 平台的每一个齿轮是怎么转的。理解了,你用什么平台都能用得更好。

Q2: 前端用 Vue / Svelte 行不行?

行。本文选 React + Next.js 是因为:

  1. 前端同学最熟
  2. shadcn/ui 组件生态最丰富
  3. AI 生成 React 代码的质量最高(训练数据多)

但架构是前后端分离的,前端换成 Vue + Nuxt、Svelte + SvelteKit 都行,后端 API 不用改。

Q3: 后端为什么不用 Node.js?

因为 Agent 运行时在 Python。OpenAI Agents SDK、LangChain、CrewAI 全是 Python 生态。用 Node 做后端意味着:

  • 要么通过 HTTP 调 Python 微服务(多一层网络)
  • 要么用 Node 的 Agent SDK(生态远不如 Python)

Python 做 Agent 运行时,Node/React 做 UI。各用各的强项。

Q4: 多用户怎么做?

开源版是单用户。多用户需要加:

# 最简方案:JWT 认证
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer

security = HTTPBearer()

async def get_current_user(token=Depends(security)):
    user = verify_jwt(token.credentials)
    if not user:
        raise HTTPException(status_code=401, detail="未授权")
    return user

# 每个 Agent 加 owner_id 字段
# 每个查询加 WHERE owner_id = ?

别一开始就做多用户。先单用户跑通,有需求再加。

Q5: 安全怎么保证?

风险对策
Agent 执行危险操作needs_approval=True + 工具白名单
用户自定义工具注入沙箱执行(Docker 容器 / gVisor)
API Key 泄露后端存储,前端永远不接触 Key
Prompt 注入输入 Guardrail + 输出 Guardrail
数据隔离每个用户的 Agent/记忆严格隔离

Q6: 这个平台能商用吗?

能,但要注意:

  • 模型 API 的 ToS(Terms of Service)——百炼和 OpenAI 都允许商用,但要遵守用量限制
  • 用户数据合规——如果涉及个人信息,需要隐私协议
  • 开源协议——如果基于本文代码,MIT 协议,随便用

Q7: 下一步做什么?

优先级功能原因
P0对话历史持久化✅ 已内置(3.5 记忆模块的 save_history / load_history
P0Agent 版本管理改坏了能回滚
P1流式输出(SSE)用户体验质的飞跃
P1多用户 + 权限团队使用的前提
P2可视化编排(React Flow)YAML 够用了,拖拽是锦上添花
P2插件市场(社区贡献)生态效应
P3移动端适配有 Web 就行,别过早优化

速记表

想做什么用什么
多模型切换LiteLLM 统一代理
Skill 动态加载数据库记录 + 运行时拼接
Agent 配置化Pydantic 数据模型 + SQLite
多 Agent 编排YAML 定义 Handoff 链
记忆持久化SQLite(长期)+ ChromaDB(语义)
工具插件化MCP 协议 + 沙箱执行
一键部署Docker Compose
非技术人员使用Web UI(Next.js)

结语

上一篇,我们造了 3 个好同事。 这一篇,我们建了一座工厂。

第 5 篇:写一份岗位说明书(Skill)
第 6 篇:招一个活人(Agent)
第 7 篇(本篇):建一座工厂(Platform)

从 30 行 Markdown 到 100 行 Python 到一整个 Web 应用——每一步都在回答同一个问题:

怎么让 AI 从"工具"变成"同事",再变成"团队"?

工厂不是一天建成的。本文给你的是蓝图和地基:

  • 6 个核心模块的数据结构和 API
  • 前端关键页面的完整代码
  • Docker 一键部署
  • 从个人到企业的商业化路径

你不需要一次全做完。 先跑通"创建 Agent → 配置 → 对话测试"这条最短路径。一个周末就够了。

然后,每加一个模块,你的工厂就多一条生产线。

直到有一天,你的同事打开浏览器,自己拖了一个 Agent 出来,试了两句话,说:"挺好用的。"

那一刻,你不再是一个写脚本的人。

你是一个造同事的人

📦 本文所有完整代码(含 Web UI 全部 7 个页面 + API Key 管理面板 + MCP 插件完整实现 + 环境搭建指南)→ 评论区扣「Agent平台」或私信获取


系列回顾:


参考来源

来源内容
OpenAI Agents SDKAgent / Handoff / Guardrails / Tracing
LiteLLM多模型统一代理(100+ 模型)
FastAPIPython Web 框架(自动 Swagger)
Next.js 15React 全栈框架(SSR / App Router)
shadcn/uiReact 组件库
React Flow可视化流程编排
ChromaDB本地向量数据库
MCP(Model Context Protocol)Anthropic 提出的工具调用标准协议
Dify / Coze / FastGPT现有 Agent 平台(对比参考)