企业内网部署本地 AI QA 工程师:Ollama + LibreChat + Playwright MCP 实战

目标:在一台企业内网 Linux 主机上,部署一套由 LibreChat、Ollama、Qwen3:8b 与 Playwright MCP 组成的浏览器测试助手。第一阶段只访问测试环境,只执行低风险、可回滚的 QA 操作,不接入生产写权限。

结论先行

这套方案适合做探索性测试、测试步骤验证和用例草拟,不适合替代确定性的 Playwright 回归测试流水线。正确的落地方式不是一开始就连接生产系统、Jira 和 GitHub,而是先在隔离测试环境里完成一个最小闭环:

  1. 用户在 LibreChat 中提交明确的测试步骤;
  2. Qwen3:8b 通过 Ollama 在内网完成推理;
  3. Playwright MCP 在隔离浏览器中逐步操作测试站点;
  4. 助手返回步骤数、通过/失败状态与证据位置;
  5. 人工复核结果,再决定是否将稳定流程改写成正式 Playwright 测试。

原文在 16 GB RAM、无独立 GPU 的环境中,简单场景启动约需 1 分钟、完整执行约需 4–5 分钟。因此,首轮试点应以隐私、可审计和探索效率为目标,而不是追求比静态脚本更快。

来源与内容边界

1. 适用范围与明确限制

适用

暂不适用

2. 最小可用架构

内网 QA 用户
    │ HTTPS / 内网统一认证
    ▼
反向代理或内网入口
    │
    ▼
LibreChat :3080
    ├── OpenAI-compatible API ──> Ollama :11434 ──> Qwen3:8b
    └── Streamable HTTP MCP ───> Playwright MCP :8931 ──> Headless Chromium
                                                       │
                                                       ▼
                                              仅允许访问测试域名

部署原则:

3. 前置条件

建议基线:

检查环境:

docker version
docker compose version
git --version
free -h
df -h

验收:以上命令均成功;可用内存和磁盘达到团队基线。

失败处理:若主机不足 16 GB RAM,不要直接扩大并发或上下文长度;先改用更小模型,或换到容量更高的试点主机。

4. 获取 LibreChat 并固定版本

sudo install -d -m 0750 /opt/local-ai-qa
sudo chown "$USER":"$USER" /opt/local-ai-qa
cd /opt/local-ai-qa

git clone https://github.com/danny-avila/LibreChat.git
cd LibreChat
cp .env.example .env
cp librechat.example.yaml librechat.yaml
cp docker-compose.override.yml.example docker-compose.override.yml

不要长期跟随浮动的 mainlatest@latest。试点验证通过后,应记录并固定:

git rev-parse HEAD
docker compose config --images

将 Git 提交号、镜像 digest、Qwen 模型 digest 写入内部变更记录。升级时重新做连通性与三个测试场景,而不是直接覆盖生产试点。

5. 配置内网服务

5.1 librechat.yaml

保留 LibreChat 示例文件中团队需要的其他配置,并合并下面的核心块:

version: 1.3.13
cache: true

endpoints:
  custom:
    - name: "Ollama"
      apiKey: "ollama"
      baseURL: "http://ollama:11434/v1/"
      models:
        default:
          - "qwen3:8b"
        fetch: true
      titleConvo: true
      titleModel: "current_model"
      summarize: false
      summaryModel: "current_model"
      modelDisplayLabel: "Ollama"

mcpSettings:
  allowedAddresses:
    - "playwright-mcp:8931"

mcpServers:
  playwright:
    type: streamable-http
    url: "http://playwright-mcp:8931/mcp"
    timeout: 120000
    initTimeout: 30000
    requiresOAuth: false
    serverInstructions: |
      仅访问管理员批准的测试域名。
      每次只执行一个浏览器工具,等待结果后再继续。
      导航后重新获取页面快照,不复用旧页面状态。
      不下载文件,不访问宿主机文件,不进行生产写操作。
      失败时停止新动作,返回当前 URL、最后成功步骤和失败原因。

为什么使用 streamable-http:当前 LibreChat 文档已经支持 streamable-http,Playwright MCP 也提供 /mcp 服务端点;原文采用的 /sse 是兼容旧客户端的路径,不应作为新部署的默认选择。

5.2 docker-compose.override.yml

services:
  api:
    volumes:
      - ./librechat.yaml:/app/librechat.yaml:ro
    depends_on:
      - ollama
      - playwright-mcp

  ollama:
    image: ollama/ollama:latest
    restart: unless-stopped
    volumes:
      - ollama-data:/root/.ollama
    expose:
      - "11434"
    environment:
      OLLAMA_NO_CLOUD: "1"
      OLLAMA_CONTEXT_LENGTH: "8192"
      OLLAMA_NUM_PARALLEL: "1"
      OLLAMA_MAX_QUEUE: "8"

  playwright-mcp:
    image: mcr.microsoft.com/playwright/mcp:latest
    restart: unless-stopped
    entrypoint: ["node", "/app/cli.js"]
    command:
      - "--headless"
      - "--browser"
      - "chromium"
      - "--no-sandbox"
      - "--host"
      - "0.0.0.0"
      - "--port"
      - "8931"
      - "--allowed-hosts"
      - "playwright-mcp:8931"
      - "--isolated"
      - "--block-service-workers"
      - "--output-mode"
      - "file"
      - "--output-dir"
      - "/tmp/playwright-output"
      - "--timeout-action"
      - "10000"
      - "--timeout-navigation"
      - "60000"
    expose:
      - "8931"
    tmpfs:
      - /tmp:size=1g,mode=1777
    security_opt:
      - no-new-privileges:true

volumes:
  ollama-data:

注意:

6. 静态检查与启动

先渲染最终 Compose 配置:

cd /opt/local-ai-qa/LibreChat
docker compose config > /tmp/local-ai-qa-compose.rendered.yaml

确认敏感服务没有发布到宿主机:

if grep -Eq 'published: (11434|8931)' /tmp/local-ai-qa-compose.rendered.yaml; then
  echo "FAIL: Ollama 或 Playwright MCP 被发布到宿主机"
  exit 1
fi

echo "PASS: 11434/8931 未发布到宿主机"

启动服务:

docker compose up -d
docker compose ps

预期:LibreChat 相关容器、ollamaplaywright-mcp 均处于 Up;不应出现反复重启。

失败处理:

docker compose logs --tail=200 api
docker compose logs --tail=200 ollama
docker compose logs --tail=200 playwright-mcp

先修复第一个明确错误,不要通过无限增大 timeout 掩盖 DNS、模型缺失或 MCP 初始化失败。

7. 下载模型并验证推理

docker compose exec ollama ollama pull qwen3:8b
docker compose exec ollama ollama list
docker compose exec ollama ollama ps

从 LibreChat API 容器验证模型 API:

docker compose exec api node -e '
fetch("http://ollama:11434/api/tags")
  .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
  .then(x => {
    const names = (x.models || []).map(m => m.name);
    if (!names.some(n => n.startsWith("qwen3:8b"))) throw new Error("qwen3:8b missing");
    console.log("PASS", names);
  })
  .catch(e => { console.error("FAIL", e.message); process.exit(1); });
'

预期输出形态:

PASS [ 'qwen3:8b' ]

然后登录 LibreChat,选择 Ollama > qwen3:8b,输入:

只回答数字:2 + 2 等于多少?

验收:回答为 4;容器日志中没有外部模型 API 请求。

8. 验证 Playwright MCP 连接

从 API 容器检查 MCP 端点可达性:

docker compose exec api node -e '
fetch("http://playwright-mcp:8931/mcp", { method: "GET" })
  .then(r => {
    console.log("HTTP", r.status, "content-type", r.headers.get("content-type"));
    if (r.status >= 500) process.exit(1);
  })
  .catch(e => { console.error("FAIL", e.message); process.exit(1); });
'

这里的 GET 不一定完成 MCP 会话初始化;它只验证容器 DNS、端口和 HTTP 服务是否可达。真正的 MCP 协议验收应在 LibreChat 中完成:

  1. 重启 API:docker compose restart api
  2. 登录 LibreChat;
  3. 创建 Agent,模型选择 Qwen3:8b
  4. 为 Agent 启用 playwright MCP;
  5. 保存后确认工具列表中出现 Playwright 浏览器工具。

失败处理:

9. 配置 QA Agent 行为约束

将下面内容放入 LibreChat Agent 的 Instructions。它保留了原文的单步调用、导航后刷新状态、语义定位和失败停止原则,并增加企业内网边界。

# 内网 QA Agent 执行规范

你是只操作测试环境的 QA 自动化助手。

## 范围
- 只执行用户明确列出的步骤,不新增业务动作。
- 只访问允许的测试域名;遇到其他域名立即停止。
- 禁止生产环境、支付、删除、审批、权限变更和批量写入。

## 工具调用
- 每次只调用一个 Playwright 工具。
- 等待当前工具返回后,再决定下一步。
- 任何交互前先获取当前页面的新快照,不猜测页面状态。

## 导航
- 点击链接、登录、提交、继续、保存或任何可能改变 URL 的动作后,等待页面加载和 URL 稳定。
- 导航后必须重新获取快照;不得继续使用旧页面引用。

## 定位
- 优先使用 role、label、text、placeholder、title、testid 等语义定位。
- 不使用 XPath、自动生成类名或 nth(),除非页面不存在更稳定的唯一标识。
- 交互前确认目标元素存在且唯一。

## 失败处理
- 任一工具失败后停止新动作。
- 重新检查当前 URL 和页面快照。
- 未确认页面状态前,不重复点击或提交。
- 最多重试一次;第二次失败后返回失败报告。

## 输出
- 返回:测试名称、总步骤数、逐步 PASS/FAIL、最后 URL、失败原因、证据文件名。
- 不能从最终页面直接证明的结果,标记为“未验证”,不得推测通过。

建议参数:Temperature 0.2、Top P 0.85。这些是原文试用参数,不是普适最优值;团队应依据固定测试集比较后再调整。

10. 三个试点场景

所有场景都应指向团队自己的隔离测试域名。下面用 https://qa.example.internal 作为占位符。

场景 A:只读页面巡检

目标:验证模型能正确导航、读取标题并停止,不产生写操作。

输入:

测试名称:首页标题巡检
允许域名:qa.example.internal
步骤:
1. 打开 https://qa.example.internal/
2. 验证页面标题包含“测试环境”
3. 返回当前 URL 和验证结果
限制:不要点击任何按钮或链接

预期输出形态:

测试名称:首页标题巡检
总步骤数:3
1. PASS - 已打开允许域名
2. PASS - 标题包含“测试环境”
3. PASS - 当前 URL: https://qa.example.internal/
结论:PASS

验收:访问域名正确、没有额外点击、结果包含明确证据而不是泛泛地说“完成”。

场景 B:测试账号登录

目标:验证表单定位、一次提交和登录后页面校验。

前置数据:由管理员创建无生产权限的测试账号;密码通过 LibreChat 的受控用户输入提供,不写入 Agent 指令、Markdown、日志或 Git。

输入:

测试名称:测试账号登录
允许域名:qa.example.internal
步骤:
1. 打开 https://qa.example.internal/login
2. 使用标签定位“用户名”和“密码”字段
3. 填入本次会话提供的测试凭据
4. 点击一次“登录”
5. 等待导航完成并重新获取页面快照
6. 验证页面出现“测试账户概览”
限制:不要修改账户资料;登录失败后不要重复提交

验收:

失败预期:若凭据错误,应返回 FAIL - 登录失败,未重试,而不是尝试猜测或更换账号。

场景 C:受控失败与恢复

目标:验证元素缺失时 Agent 会停止并报告,而不是幻觉点击。

输入:

测试名称:缺失元素处理
允许域名:qa.example.internal
步骤:
1. 打开 https://qa.example.internal/
2. 查找文本为“这个按钮不存在”的按钮
3. 如果不存在,停止测试并返回当前 URL、页面标题和失败原因
限制:不要点击其他相似按钮,不要使用 nth() 猜测目标

预期输出形态:

测试名称:缺失元素处理
总步骤数:3
1. PASS - 页面已打开
2. FAIL - 未找到唯一且可见的“这个按钮不存在”按钮
3. PASS - 已停止,没有执行替代点击
结论:FAIL(符合失败处理预期)

验收:没有替代点击、没有重复查找循环、没有把“找不到元素”改写成成功。

11. 一键预检脚本

创建 /opt/local-ai-qa/LibreChat/scripts/preflight-local-ai-qa.sh

#!/usr/bin/env bash
set -euo pipefail

cd "$(dirname "$0")/.."

docker compose config >/tmp/local-ai-qa-compose.rendered.yaml

if grep -Eq 'published: (11434|8931)' /tmp/local-ai-qa-compose.rendered.yaml; then
  echo "FAIL: sensitive service port published to host"
  exit 1
fi

docker compose ps --status running

docker compose exec -T api node -e '
Promise.all([
  fetch("http://ollama:11434/api/tags").then(r => {
    if (!r.ok) throw new Error(`ollama HTTP ${r.status}`);
    return r.json();
  }),
  fetch("http://playwright-mcp:8931/mcp").then(r => {
    if (r.status >= 500) throw new Error(`mcp HTTP ${r.status}`);
    return r.status;
  })
]).then(([tags, mcpStatus]) => {
  const names = (tags.models || []).map(m => m.name);
  if (!names.some(n => n.startsWith("qwen3:8b"))) {
    throw new Error("qwen3:8b missing");
  }
  console.log(JSON.stringify({
    status: "PASS",
    model: "qwen3:8b",
    mcp_http_status: mcpStatus
  }));
}).catch(e => {
  console.error(JSON.stringify({status: "FAIL", error: e.message}));
  process.exit(1);
});
'

授权并运行:

chmod 0750 scripts/preflight-local-ai-qa.sh
./scripts/preflight-local-ai-qa.sh

预期输出形态:

{"status":"PASS","model":"qwen3:8b","mcp_http_status":406}

mcp_http_status 的具体非 5xx 状态可能随协议版本变化;预检只证明 HTTP 服务可达,不能替代 LibreChat 中的真实工具调用验收。

12. 内网安全与合规清单

上线试点前逐项确认:

特别注意:Playwright MCP 的 --allowed-hosts 用于服务端 Host/DNS rebinding 检查,不是浏览器访问目标的完整网络隔离。真正的目标域名限制应由防火墙、出站代理、网络命名空间或专用测试网段执行。

13. 无互联网内网的离线转运

在可联网的受控准备机上:

docker pull ollama/ollama:latest
docker pull mcr.microsoft.com/playwright/mcp:latest
# LibreChat 仓库 compose 所需镜像也需先拉取:
docker compose pull

docker save -o local-ai-qa-images.tar \
  ollama/ollama:latest \
  mcr.microsoft.com/playwright/mcp:latest
sha256sum local-ai-qa-images.tar > local-ai-qa-images.tar.sha256

模型转运建议使用企业批准的内部模型仓库。若只能转移 Ollama 数据卷,应在准备机完成 ollama pull qwen3:8b,停止写入后导出数据卷,并在目标机恢复到新建卷;具体命令要根据企业备份工具和 Docker 数据目录制定,不要直接复制运行中的卷。

在内网目标机至少执行:

sha256sum -c local-ai-qa-images.tar.sha256
docker load -i local-ai-qa-images.tar

验收:校验和一致;目标机能够在无公网访问条件下启动镜像、列出 qwen3:8b 并通过预检。

14. 四周试点计划

第 1 周:单机闭环

第 2 周:小组试用

第 3 周:固化边界

第 4 周:有限扩展

15. 验收标准

试点最低通过线:

  1. 部署:所有容器稳定运行,敏感端口未对宿主机发布;
  2. 推理:Qwen3:8b 可在内网调用,Ollama 云功能关闭;
  3. 工具:LibreChat 能连接 Playwright MCP,并完成真实浏览器调用;
  4. 行为:场景 A、B 正常完成,场景 C 按预期失败并停止;
  5. 安全:无生产凭据、无生产写权限、无未批准公网出口;
  6. 证据:每次运行能给出步骤级 PASS/FAIL、最终 URL 和失败原因;
  7. 性能:团队接受实测延迟;若不能接受,优先调整模型、硬件或场景,不把探索性 Agent 当静态回归测试替代品。

16. 回滚

停止并保留数据用于排查:

cd /opt/local-ai-qa/LibreChat
docker compose down

回滚配置:

git status
git diff -- librechat.yaml docker-compose.override.yml
git checkout -- librechat.yaml docker-compose.override.yml

若这些文件是企业自有配置,不应直接 git checkout;应从已评审备份或配置仓库恢复。确认不再需要数据后,删除模型卷属于破坏性操作,必须走审批,不要在普通故障处理中执行 docker compose down -v

17. 原文方法覆盖清单

原文方法 是否纳入 落点 若遗漏原因
Docker 部署 LibreChat 第 4–6 节
Ollama 安装/服务与 Qwen3:8b 第 5、7 节 改为 Compose 内部服务,避免内网暴露 API
LibreChat 连接 Ollama 第 5.1 节
Playwright MCP 服务 第 5.2、8 节 使用当前 Streamable HTTP;保留 SSE 回退说明
MCP 允许地址配置 第 5.1 节 使用当前 allowedAddresses 精确地址规则
创建 LibreChat Agent 第 8、9 节
单工具串行、导航后刷新、语义定位、失败停止 第 9 节
Playwright 首页验证 场景 A 替换为企业测试域名占位符
ParaBank 登录示例 是,改写 场景 B 改为企业测试账号,避免依赖公网演示站
Jira/GitHub/数据库扩展 暂不纳入执行 第 1、14 节 首阶段只读低风险试点,不增加写权限
macOS Homebrew 安装 Ollama 本教程目标为企业内网 Linux 容器部署
旧版 SSE /sse 默认连接 否,保留回退 第 5.1、8 节 当前 LibreChat 支持 Streamable HTTP,新部署优先 /mcp
--allowed-hosts "*" 第 5.2 节 企业部署不应关闭 Host 检查

18. 证据级别

参考资料