上一篇 下一篇 分享链接 返回 返回顶部

从零把 AI 浏览器跑上生产:Playwright 部署、安全加固与源码实战

发布人:慈云数据-客服中心 发布时间:7小时前 阅读量:0

AI浏览器 生产环境部署指南|附源码

本文将从架构设计、环境准备、后端服务、浏览器自动化、安全加固、Docker 部署、Nginx 反向代理、日志监控、性能优化等方面,完整讲解如何将一个“AI 浏览器”项目部署到生产环境。文末附带可直接运行的核心源码示例,适合用于二次开发、企业内部工具、AI Agent 浏览器执行器、网页自动化测试平台等场景。


一、什么是 AI 浏览器?

所谓 AI 浏览器,并不是传统意义上的 Chrome、Edge 或 Safari 浏览器,而是一个能够被 AI 调用、控制和理解网页内容的浏览器执行环境。

它通常具备以下能力:

  1. 打开指定网页
  2. 读取网页 DOM 内容
  3. 截图或生成页面快照
  4. 点击按钮、输入文本、滚动页面
  5. 提取网页中的结构化信息
  6. 结合大模型完成任务规划
  7. 将浏览器操作结果返回给用户或业务系统

在实际业务中,AI 浏览器可以应用于:

  • 自动化网页数据采集
  • AI Agent 网页操作执行器
  • 表单自动填写
  • 订单状态查询
  • 网页内容摘要
  • 自动化测试
  • 内部管理后台辅助操作
  • RPA 智能化升级

举个例子,用户输入:

帮我打开某个商品页面,提取商品标题、价格、库存状态,并截图保存。

AI 浏览器就可以自动打开网页,等待页面加载,解析页面内容,截取页面图片,并返回结构化数据。


二、生产环境部署的核心问题

在本地开发环境中,我们通常直接运行:

npm run dev

或者:

node server.js

但生产环境部署远比本地运行复杂,需要考虑以下问题:

问题 说明
稳定性 服务不能频繁崩溃,需要进程守护
并发控制 浏览器实例非常消耗资源,需要限制任务数量
安全性 不能让外部用户随意访问内网、执行危险操作
资源隔离 每个浏览器任务应尽量隔离上下文
日志追踪 每个任务的执行过程要可观测
超时机制 防止页面长时间卡死
容器化 方便迁移、扩容和回滚
反向代理 统一 HTTPS、域名和限流
监控告警 CPU、内存、任务失败率需要监控

因此,生产环境的 AI 浏览器部署不能只是“能跑”,而是要做到:

安全、稳定、可观测、可扩展、可维护。


三、整体架构设计

本文示例采用如下技术栈:

  • Node.js:提供 HTTP API 服务
  • Express:构建后端接口
  • Playwright:控制 Chromium 浏览器
  • Docker:容器化部署
  • Docker Compose:编排服务
  • Nginx:反向代理、HTTPS、限流
  • PM2 / Docker restart policy:进程守护
  • Redis:可选,用于任务队列和并发控制
  • Prometheus / Grafana:可选,用于监控

基础架构如下:

用户 / 业务系统
      |
      v
   Nginx
      |
      v
AI Browser API Service
      |
      v
 Playwright Chromium
      |
      v
目标网页

如果是更高并发的生产环境,可以进一步拆分为:

Client
  |
Nginx / API Gateway
  |
AI Browser API
  |
Task Queue Redis
  |
Browser Worker Cluster
  |
Chromium / Browser Context

四、服务器环境要求

AI 浏览器对服务器资源要求相对较高,尤其是内存和 CPU。

1. 推荐配置

场景 CPU 内存 磁盘
测试环境 2 核 4 GB 30 GB
小型生产 4 核 8 GB 50 GB
中型生产 8 核 16 GB 100 GB
高并发生产 16 核以上 32 GB 以上 200 GB 以上

2. 系统推荐

建议使用:

Ubuntu 22.04 LTS

因为 Ubuntu 对 Docker、Playwright、Chromium 依赖支持较好。

3. 安装基础软件

sudo apt update
sudo apt install -y curl wget git vim ufw ca-certificates gnupg

安装 Docker:

curl -fsSL https://get.docker.com | bash
sudo systemctl enable docker
sudo systemctl start docker

安装 Docker Compose:

sudo apt install -y docker-compose-plugin

验证:

docker --version
docker compose version

五、项目目录结构

下面是一个适合生产部署的 AI 浏览器项目结构:

ai-browser/
├── src/
│   ├── app.js
│   ├── browser.js
│   ├── config.js
│   ├── middleware/
│   │   ├── auth.js
│   │   └── error.js
│   └── routes/
│       └── browser.route.js
├── logs/
├── screenshots/
├── Dockerfile
├── docker-compose.yml
├── nginx.conf
├── package.json
├── .env.example
└── README.md

目录说明:

文件/目录 说明
src/app.js Express 服务入口
src/browser.js Playwright 浏览器控制逻辑
src/config.js 配置文件
middleware/auth.js API Key 鉴权
middleware/error.js 错误处理中间件
routes/browser.route.js 浏览器相关接口
screenshots/ 截图保存目录
logs/ 日志目录
Dockerfile 容器构建文件
docker-compose.yml 容器编排文件
nginx.conf Nginx 反向代理配置

六、后端接口设计

生产环境中,不建议让前端直接调用底层 Playwright 操作,而是封装成明确的 API。

示例接口如下:

方法 路径 功能
GET /health 健康检查
POST /api/browser/open 打开网页并返回标题
POST /api/browser/screenshot 打开网页并截图
POST /api/browser/extract 提取网页文本内容
POST /api/browser/action 执行点击、输入等操作

请求示例:

curl -X POST https://browser.example.com/api/browser/screenshot \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret-key" \
  -d '{
    "url": "https://example.com",
    "fullPage": true
  }'

返回示例:

{
  "success": true,
  "data": {
    "title": "Example Domain",
    "screenshot": "/screenshots/1700000000000.png"
  }
}

七、核心源码

下面给出一个可运行的基础版本源码。


1. package.json

{
  "name": "ai-browser",
  "version": "1.0.0",
  "description": "AI Browser production deployment demo with Playwright",
  "main": "src/app.js",
  "scripts": {
    "start": "node src/app.js",
    "dev": "node --watch src/app.js"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "dotenv": "^16.4.5",
    "express": "^4.18.3",
    "helmet": "^7.1.0",
    "morgan": "^1.10.0",
    "playwright": "^1.42.1",
    "uuid": "^9.0.1"
  }
}

2. .env.example

PORT=3000
API_KEY=change-this-secret-key
NODE_ENV=production
BROWSER_HEADLESS=true
REQUEST_TIMEOUT=30000
MAX_TEXT_LENGTH=8000
SCREENSHOT_DIR=./screenshots

生产环境中请复制一份:

cp .env.example .env

然后修改:

API_KEY=一个足够复杂的密钥

3. src/config.js

require("dotenv").config();

const config = {
  port: Number(process.env.PORT || 3000),
  apiKey: process.env.API_KEY || "change-this-secret-key",
  nodeEnv: process.env.NODE_ENV || "development",
  browserHeadless: process.env.BROWSER_HEADLESS !== "false",
  requestTimeout: Number(process.env.REQUEST_TIMEOUT || 30000),
  maxTextLength: Number(process.env.MAX_TEXT_LENGTH || 8000),
  screenshotDir: process.env.SCREENSHOT_DIR || "./screenshots"
};

module.exports = config;

4. src/middleware/auth.js

const config = require("../config");

function auth(req, res, next) {
  const apiKey = req.headers["x-api-key"];

  if (!apiKey || apiKey !== config.apiKey) {
    return res.status(401).json({
      success: false,
      message: "Unauthorized"
    });
  }

  next();
}

module.exports = auth;

5. src/middleware/error.js

function errorHandler(err, req, res, next) {
  console.error("[ERROR]", err);

  res.status(err.status || 500).json({
    success: false,
    message: err.message || "Internal Server Error"
  });
}

module.exports = errorHandler;

6. src/browser.js

const { chromium } = require("playwright");
const path = require("path");
const fs = require("fs");
const { v4: uuidv4 } = require("uuid");
const config = require("./config");

let browserPromise = null;

async function getBrowser() {
  if (!browserPromise) {
    browserPromise = chromium.launch({
      headless: config.browserHeadless,
      args: [
        "--no-sandbox",
        "--disable-dev-shm-usage",
        "--disable-gpu",
        "--disable-setuid-sandbox",
        "--disable-web-security"
      ]
    });
  }

  return browserPromise;
}

function validateUrl(url) {
  if (!url) {
    throw new Error("URL is required");
  }

  const parsed = new URL(url);

  if (!["http:", "https:"].includes(parsed.protocol)) {
    throw new Error("Only http and https protocols are allowed");
  }

  /**
   * 生产环境建议增加 SSRF 防护:
   * 1. 禁止访问 127.0.0.1
   * 2. 禁止访问 localhost
   * 3. 禁止访问内网 IP
   * 4. 使用 DNS 解析后校验真实 IP
   */
  const hostname = parsed.hostname;

  const blockedHosts = [
    "localhost",
    "127.0.0.1",
    "0.0.0.0"
  ];

  if (blockedHosts.includes(hostname)) {
    throw new Error("Access to local addresses is not allowed");
  }

  return parsed.toString();
}

async function createPage() {
  const browser = await getBrowser();

  const context = await browser.newContext({
    viewport: {
      width: 1365,
      height: 768
    },
    userAgent:
      "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
      "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
  });

  const page = await context.newPage();

  page.setDefaultTimeout(config.requestTimeout);
  page.setDefaultNavigationTimeout(config.requestTimeout);

  return {
    context,
    page
  };
}

async function openUrl(url) {
  const safeUrl = validateUrl(url);
  const { context, page } = await createPage();

  try {
    await page.goto(safeUrl, {
      waitUntil: "domcontentloaded",
      timeout: config.requestTimeout
    });

    const title = await page.title();

    return {
      title,
      url: page.url()
    };
  } finally {
    await context.close();
  }
}

async function screenshot(url, fullPage = true) {
  const safeUrl = validateUrl(url);
  const { context, page } = await createPage();

  try {
    await page.goto(safeUrl, {
      waitUntil: "networkidle",
      timeout: config.requestTimeout
    });

    if (!fs.existsSync(config.screenshotDir)) {
      fs.mkdirSync(config.screenshotDir, {
        recursive: true
      });
    }

    const fileName = `${Date.now()}-${uuidv4()}.png`;
    const filePath = path.join(config.screenshotDir, fileName);

    await page.screenshot({
      path: filePath,
      fullPage
    });

    const title = await page.title();

    return {
      title,
      fileName,
      path: filePath
    };
  } finally {
    await context.close();
  }
}

async function extractText(url) {
  const safeUrl = validateUrl(url);
  const { context, page } = await createPage();

  try {
    await page.goto(safeUrl, {
      waitUntil: "domcontentloaded",
      timeout: config.requestTimeout
    });

    const title = await page.title();

    let text = await page.locator("body").innerText({
      timeout: config.requestTimeout
    });

    if (text.length > config.maxTextLength) {
      text = text.slice(0, config.maxTextLength);
    }

    return {
      title,
      url: page.url(),
      text
    };
  } finally {
    await context.close();
  }
}

async function performActions(url, actions = []) {
  const safeUrl = validateUrl(url);
  const { context, page } = await createPage();

  try {
    await page.goto(safeUrl, {
      waitUntil: "domcontentloaded",
      timeout: config.requestTimeout
    });

    for (const action of actions) {
      if (action.type === "click") {
        await page.click(action.selector);
      }

      if (action.type === "fill") {
        await page.fill(action.selector, action.value || "");
      }

      if (action.type === "wait") {
        await page.waitForTimeout(Number(action.ms || 1000));
      }

      if (action.type === "press") {
        await page.press(action.selector, action.key);
      }
    }

    const title = await page.title();
    const text = await page.locator("body").innerText();

    return {
      title,
      url: page.url(),
      text: text.slice(0, config.maxTextLength)
    };
  } finally {
    await context.close();
  }
}

async function closeBrowser() {
  if (browserPromise) {
    const browser = await browserPromise;
    await browser.close();
    browserPromise = null;
  }
}

module.exports = {
  openUrl,
  screenshot,
  extractText,
  performActions,
  closeBrowser
};

7. src/routes/browser.route.js

const express = require("express");
const {
  openUrl,
  screenshot,
  extractText,
  performActions
} = require("../browser");

const router = express.Router();

router.post("/open", async (req, res, next) => {
  try {
    const { url } = req.body;

    const data = await openUrl(url);

    res.json({
      success: true,
      data
    });
  } catch (err) {
    next(err);
  }
});

router.post("/screenshot", async (req, res, next) => {
  try {
    const { url, fullPage } = req.body;

    const data = await screenshot(url, fullPage !== false);

    res.json({
      success: true,
      data
    });
  } catch (err) {
    next(err);
  }
});

router.post("/extract", async (req, res, next) => {
  try {
    const { url } = req.body;

    const data = await extractText(url);

    res.json({
      success: true,
      data
    });
  } catch (err) {
    next(err);
  }
});

router.post("/action", async (req, res, next) => {
  try {
    const { url, actions } = req.body;

    const data = await performActions(url, actions || []);

    res.json({
      success: true,
      data
    });
  } catch (err) {
    next(err);
  }
});

module.exports = router;

8. src/app.js

const express = require("express");
const cors = require("cors");
const helmet = require("helmet");
const morgan = require("morgan");
const path = require("path");

const config = require("./config");
const auth = require("./middleware/auth");
const errorHandler = require("./middleware/error");
const browserRoute = require("./routes/browser.route");
const { closeBrowser } = require("./browser");

const app = express();

app.use(helmet());
app.use(cors());
app.use(express.json({
  limit: "1mb"
}));
app.use(morgan("combined"));

app.get("/health", (req, res) => {
  res.json({
    success: true,
    status: "ok",
    uptime: process.uptime()
  });
});

app.use("/screenshots", express.static(path.resolve(config.screenshotDir)));

app.use("/api/browser", auth, browserRoute);

app.use(errorHandler);

const server = app.listen(config.port, () => {
  console.log(`AI Browser service running on port ${config.port}`);
});

async function shutdown() {
  console.log("Shutting down AI Browser service...");

  server.close(async () => {
    await closeBrowser();
    process.exit(0);
  });
}

process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);

八、本地运行测试

安装依赖:

npm install

安装 Playwright 浏览器:

npx playwright install chromium

启动服务:

npm run dev

测试健康检查:

curl http://localhost:3000/health

测试截图接口:

curl -X POST http://localhost:3000/api/browser/screenshot \
  -H "Content-Type: application/json" \
  -H "X-API-Key: change-this-secret-key" \
  -d '{
    "url": "https://example.com",
    "fullPage": true
  }'

九、Dockerfile 编写

生产环境推荐使用 Docker 部署。Playwright 官方提供了带浏览器依赖的镜像,可以避免手动安装大量 Chromium 依赖。

Dockerfile

FROM mcr.microsoft.com/playwright:v1.42.1-jammy

WORKDIR /app

COPY package*.json ./

RUN npm install --omit=dev

COPY . .

RUN mkdir -p /app/screenshots /app/logs

ENV NODE_ENV=production

EXPOSE 3000

CMD ["npm", "start"]

构建镜像:

docker build -t ai-browser:1.0.0 .

运行容器:

docker run -d \
  --name ai-browser \
  -p 3000:3000 \
  --env-file .env \
  -v $(pwd)/screenshots:/app/screenshots \
  --restart always \
  ai-browser:1.0.0

查看日志:

docker logs -f ai-browser

十、Docker Compose 部署

docker-compose.yml

services:
  ai-browser:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: ai-browser
    restart: always
    env_file:
      - .env
    ports:
      - "3000:3000"
    volumes:
      - ./screenshots:/app/screenshots
      - ./logs:/app/logs
    shm_size: "1gb"
    security_opt:
      - seccomp=unconfined
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

启动:

docker compose up -d --build

查看状态:

docker compose ps

查看日志:

docker compose logs -f

停止:

docker compose down

升级:

git pull
docker compose up -d --build

十一、Nginx 反向代理配置

生产环境通常不会直接暴露 3000 端口,而是通过 Nginx 统一代理。

nginx.conf 示例

server {
    listen 80;
    server_name browser.example.com;

    client_max_body_size 2m;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_connect_timeout 30s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

启用配置:

sudo ln -s /etc/nginx/sites-available/ai-browser.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

如果需要 HTTPS,推荐使用 Certbot:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d browser.example.com

十二、生产环境安全加固

AI 浏览器的安全非常重要,因为它具备访问网页、执行点击、输入内容等能力。如果没有限制,可能被恶意利用。

1. API Key 鉴权

本文示例已经实现:

X-API-Key: your-secret-key

生产环境建议:

  • 使用足够长的随机密钥
  • 定期轮换密钥
  • 不要把密钥写死在前端代码中
  • 通过后端服务间调用

2. SSRF 防护

AI 浏览器可以访问 URL,因此必须防止访问内网地址。例如:

  • http://127.0.0.1
  • http://localhost
  • http://169.254.169.254
  • http://10.0.0.1
  • http://192.168.1.1
  • http://172.16.0.1

生产级实现应该对域名解析后的 IP 做检查,防止通过 DNS 绕过。

3. 限制请求体大小

示例中配置了:

app.use(express.json({
  limit: "1mb"
}));

防止用户提交过大的请求体导致内存占用过高。

4. 限制并发

浏览器任务非常消耗资源。即使服务器配置较高,也不能无限创建页面。

可以使用简单信号量控制并发,例如限制同时最多执行 5 个任务。

class Semaphore {
  constructor(max) {
    this.max = max;
    this.current = 0;
    this.queue = [];
  }

  async acquire() {
    if (this.current < this.max) {
      this.current++;
      return;
    }

    await new Promise(resolve => this.queue.push(resolve));
    this.current++;
  }

  release() {
    this.current--;

    const next = this.queue.shift();
    if (next) {
      next();
    }
  }
}

module.exports = new Semaphore(5);

然后在接口执行前后调用:

await semaphore.acquire();

try {
  // 执行浏览器任务
} finally {
  semaphore.release();
}

5. 禁止危险操作

如果开放 /action 接口,建议不要允许用户任意执行 JavaScript,例如:

page.evaluate(userInput)

这是非常危险的,因为用户可以构造恶意脚本。

建议只开放有限动作:

  • click
  • fill
  • press
  • wait
  • screenshot
  • extractText

十三、性能优化建议

1. 复用 Browser,隔离 Context

不要每次请求都启动一个新的 Chromium 进程。启动浏览器成本很高。

推荐方式:

  • 全局复用 browser
  • 每个请求创建新的 context
  • 请求结束关闭 context

本文源码就是这种方式:

const browser = await getBrowser();
const context = await browser.newContext();

这样既能提升性能,也能保证 cookie、localStorage 等上下文隔离。

2. 设置超时时间

所有页面导航和元素操作都应该设置超时时间:

page.setDefaultTimeout(30000);
page.setDefaultNavigationTimeout(30000);

否则页面异常时可能长期占用资源。

3. 合理使用 waitUntil

常见选项:

选项 说明
load 等待 load 事件
domcontentloaded DOM 加载完成即可
networkidle 网络空闲,适合截图
commit 导航提交即可

如果只是提取标题或正文,推荐:

waitUntil: "domcontentloaded"

如果要截图,推荐:

waitUntil: "networkidle"

networkidle 在部分网站可能等待较久,因此要结合超时时间使用。

4. 定期清理截图文件

截图文件长期保存会占满磁盘。可以使用定时任务清理 7 天前的图片:

find /path/to/ai-browser/screenshots -type f -mtime +7 -delete

加入 crontab:

crontab -e

添加:

0 3 * * * find /path/to/ai-browser/screenshots -type f -mtime +7 -delete

十四、日志与监控

生产环境不能只依靠 console.log。至少需要关注:

  • 请求耗时
  • 错误堆栈
  • 任务成功率
  • 浏览器崩溃次数
  • CPU 使用率
  • 内存使用率
  • 磁盘剩余空间
  • 截图目录大小

如果使用 Docker,可以先通过以下命令排查:

docker stats ai-browser
docker logs -f ai-browser

如果需要更完整的监控体系,可以使用:

  • Prometheus
  • Grafana
  • Loki
  • ELK
  • OpenTelemetry

十五、常见问题排查

1. Chromium 启动失败

常见原因是系统依赖缺失。推荐直接使用 Playwright 官方 Docker 镜像:

FROM mcr.microsoft.com/playwright:v1.42.1-jammy

2. 页面截图为空白

可能原因:

  • 页面还没加载完成
  • 目标网站有反爬机制
  • 页面依赖登录态
  • CSS 或字体加载失败

可以尝试:

await page.goto(url, {
  waitUntil: "networkidle"
});
await page.waitForTimeout(1000);

3. Docker 中浏览器崩溃

可增加共享内存:

shm_size: "1gb"

或者启动参数增加:

"--disable-dev-shm-usage"

4. 请求偶发超时

可能是目标网站响应慢,也可能是服务器并发过高。

建议:

  • 增加超时时间
  • 降低并发
  • 增加任务队列
  • 横向扩容 Worker

5. 访问部分网站被拦截

部分网站会识别自动化浏览器。可优化:

  • 设置合理 User-Agent
  • 使用真实浏览器上下文
  • 降低访问频率
  • 避免异常操作模式
  • 必要时接入代理池

注意:实际业务中应遵守目标网站的服务条款与法律法规。


十六、上线检查清单

上线前建议逐项检查:

  • [ ] .env 中 API Key 已修改
  • [ ] Nginx 已开启 HTTPS
  • [ ] 防火墙未直接暴露不必要端口
  • [ ] Docker 设置了 restart: always
  • [ ] 截图目录已挂载到宿主机
  • [ ] 日志可查看
  • [ ] 设置了请求超时
  • [ ] 设置了接口鉴权
  • [ ] 设置了 SSRF 防护
  • [ ] 设置了并发限制
  • [ ] 设置了磁盘清理任务
  • [ ] 健康检查接口正常
  • [ ] 压测通过
  • [ ] 备份和回滚方案明确

十七、与大模型集成的思路

AI 浏览器通常不是单独存在,而是作为大模型 Agent 的工具之一。

例如,可以把接口封装成工具:

{
  "name": "browser_extract",
  "description": "打开网页并提取正文内容",
  "parameters": {
    "url": "需要访问的网页地址"
  }
}

当用户说:

总结这个网页的主要内容:https://example.com

大模型可以先调用 AI 浏览器提取网页正文,再对正文进行总结。

更复杂的 Agent 可以支持:

  1. 规划任务步骤
  2. 调用浏览器打开页面
  3. 观察页面内容
  4. 判断下一步点击或输入
  5. 多轮执行
  6. 生成最终结果

这类系统通常需要额外设计:

  • 工具调用协议
  • 页面观察格式
  • 操作白名单
  • 任务状态机
  • 多轮上下文管理
  • 错误恢复机制

十八、总结

本文完整介绍了一个 AI 浏览器从开发到生产部署的基本方案,并提供了可运行的 Node.js + Playwright 源码示例。

在生产环境中,AI 浏览器的重点不是简单地“打开网页”,而是要解决:

  • 如何稳定运行
  • 如何隔离浏览器上下文
  • 如何防止恶意访问
  • 如何控制资源消耗
  • 如何记录日志和排查问题
  • 如何通过 Docker 和 Nginx 实现标准化部署

如果只是内部低频使用,本文提供的单服务版本已经足够;如果要面向高并发业务,则建议进一步演进为“API 服务 + Redis 队列 + Worker 集群”的架构。

最终,一个可靠的 AI 浏览器服务应该具备以下特征:

API 清晰、权限严格、并发可控、任务可观测、部署可重复、故障可恢复。

有了这套基础设施,后续无论是接入大模型 Agent、构建自动化采集系统,还是开发企业内部智能助手,都可以在此基础上快速扩展。

目录结构
全文