从零搭一条可复现的 AGN 远端执行链路

这篇文章只做一件事:把一台远端 Ubuntu VM 变成一个最小可用的 AGN 执行层。最终效果是:

  • 远端 Ubuntu 上有常驻 worker
  • 本地 Mac 可以通过 SSH 提交任务、读状态、取结果
  • OpenClaw 安装在远端 VM 上
  • OpenClaw 最终能在远端 Ubuntu 上实际创建 Rust 项目、修改源码、运行 cargo run,并把产物写到固定文件
  • 本地可以把 REPORT.mdCargo.tomlmain.rs 拉回来看

整条链路的最终验证结果是成立的。


占位符约定

先把下面这些替换成你自己的值:

<VM_USER>
<VM_HOST>
<VM_IP>
<GOOGLE_API_KEY>
<OPENAI_API_KEY>
<YOUR_EMAIL>

文中默认:

  • 远端 Ubuntu VM 用户名是 <VM_USER>
  • 远端机器地址是 <VM_IP>
  • 本地机器是 macOS
  • 远端工作目录是 ~/agn-lab
  • 本地 client 目录是 ~/agn-client

1. 在远端 Ubuntu 上安装基础环境

在 VM SSH 中执行:

sudo apt update && sudo apt -y upgrade && sudo apt -y install git curl wget unzip zip tmux htop build-essential python3 python3-venv python3-pip

安装 Rust:

curl https://sh.rustup.rs -sSf | sh -s -- -y

加载 Rust 环境并检查版本:

source "$HOME/.cargo/env" && rustc --version && cargo --version

验证 Rust 可运行:

source "$HOME/.cargo/env" && cargo new hello-rust && cd hello-rust && cargo run

检查当前 Rust toolchain:

source "$HOME/.cargo/env" && rustup show

这是整条链路的最底层依赖之一,后面远端 agent 的 Rust 任务验证直接依赖这一步。


2. 创建 AGN 目录结构

在 VM SSH 中执行:

mkdir -p ~/agn-lab/{tasks,outputs,state,logs,scripts}
cd ~/agn-lab

这套目录结构很简单:

  • tasks/:待处理任务
  • outputs/:任务结果
  • state/:任务状态
  • logs/:worker 日志
  • scripts/:稳定接口和 worker 脚本

3. 写任务提交与读取接口

submit_task.sh

在 VM SSH 中执行:

cd ~/agn-lab && cat > scripts/submit_task.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
BASE_DIR="$HOME/agn-lab"
TASK_DIR="$BASE_DIR/tasks"
mkdir -p "$TASK_DIR"
TASK_ID="${1:?missing task_id}"
MODEL="${2:?missing model}"
PROMPT="${3:?missing prompt}"
python3 -c 'import json,sys; print(json.dumps({"task_id":sys.argv[1],"model":sys.argv[2],"prompt":sys.argv[3]}, ensure_ascii=False))' "$TASK_ID" "$MODEL" "$PROMPT" > "$TASK_DIR/${TASK_ID}.json"
echo "$TASK_DIR/${TASK_ID}.json"
EOF
chmod +x scripts/submit_task.sh

read_state.sh

cd ~/agn-lab && cat > scripts/read_state.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
TASK_ID="${1:?missing task_id}"
STATE_FILE="$HOME/agn-lab/state/${TASK_ID}.state"

if [ -f "$STATE_FILE" ]; then
  cat "$STATE_FILE"
else
  echo "missing"
fi
EOF
chmod +x scripts/read_state.sh

read_result.sh

cd ~/agn-lab && cat > scripts/read_result.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
TASK_ID="${1:?missing task_id}"
RESULT_FILE="$HOME/agn-lab/outputs/${TASK_ID}.txt"
STATE_FILE="$HOME/agn-lab/state/${TASK_ID}.state"

if [ -f "$RESULT_FILE" ]; then
  cat "$RESULT_FILE"
elif [ -f "$STATE_FILE" ]; then
  cat "$STATE_FILE"
else
  echo "missing"
fi
EOF
chmod +x scripts/read_result.sh

这里固定了后续所有上层系统的边界:外部系统只需要提交任务、读状态、读结果,不直接碰目录结构。这个接口设计在后面接 OpenClaw 时保持不变。


4. 写 JSON API 包装层

在 VM SSH 中执行:

cd ~/agn-lab && cat > scripts/task_api_json.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

ACTION="${1:?missing action}"

case "$ACTION" in
  submit)
    TASK_ID="${2:?missing task_id}"
    MODEL="${3:?missing model}"
    PROMPT="${4:?missing prompt}"
    "$HOME/agn-lab/scripts/submit_task.sh" "$TASK_ID" "$MODEL" "$PROMPT" >/dev/null
    python3 -c 'import json,sys; print(json.dumps({"ok":True,"action":"submit","task_id":sys.argv[1]}))' "$TASK_ID"
    ;;
  state)
    TASK_ID="${2:?missing task_id}"
    STATE="$("$HOME/agn-lab/scripts/read_state.sh" "$TASK_ID")"
    python3 -c 'import json,sys; print(json.dumps({"ok":True,"action":"state","task_id":sys.argv[1],"state":sys.argv[2]}))' "$TASK_ID" "$STATE"
    ;;
  result)
    TASK_ID="${2:?missing task_id}"
    RESULT="$("$HOME/agn-lab/scripts/read_result.sh" "$TASK_ID")"
    python3 -c 'import json,sys; print(json.dumps({"ok":True,"action":"result","task_id":sys.argv[1],"result":sys.argv[2]}, ensure_ascii=False))' "$TASK_ID" "$RESULT"
    ;;
  *)
    python3 -c 'import json; print(json.dumps({"ok":False,"error":"invalid_action"}))'
    exit 1
    ;;
esac
EOF
chmod +x scripts/task_api_json.sh

先测接口外壳:

cd ~/agn-lab && ./scripts/task_api_json.sh submit t010 gemini-3.1-pro-preview "Explain in 2 concise sentences what SSH orchestration is."
cd ~/agn-lab && ./scripts/task_api_json.sh state t010
cd ~/agn-lab && sleep 8 && ./scripts/task_api_json.sh result t010

这层是后面本地 Mac 和远端 worker 之间最稳的 JSON 边界。

ChatGPT-GPT-5.4 数学能力分析 (1)


5. 写 Gemini worker

这一步的目标不是做复杂 agent,只是做一个最小可运行的、文件驱动的、单机 executor。文件里确认这条路径后来已经能稳定处理 submit -> running -> processed -> result

worker_gemini_loop.sh

在 VM SSH 中执行:

cd ~/agn-lab && cat > scripts/worker_gemini_loop.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

BASE_DIR="$HOME/agn-lab"
TASK_DIR="$BASE_DIR/tasks"
STATE_DIR="$BASE_DIR/state"
OUT_DIR="$BASE_DIR/outputs"
LOG_DIR="$BASE_DIR/logs"

mkdir -p "$TASK_DIR" "$STATE_DIR" "$OUT_DIR" "$LOG_DIR"

while true; do
  for TASK_FILE in "$TASK_DIR"/*.json; do
    [ -e "$TASK_FILE" ] || continue

    TASK_ID="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["task_id"])' "$TASK_FILE")"
    MODEL="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("model","gemini-3.1-pro-preview"))' "$TASK_FILE")"
    PROMPT="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["prompt"])' "$TASK_FILE")"

    STATE_FILE="$STATE_DIR/${TASK_ID}.state"
    RESULT_FILE="$OUT_DIR/${TASK_ID}.txt"

    if [ -f "$STATE_FILE" ] && grep -qx 'processed' "$STATE_FILE"; then
      rm -f "$TASK_FILE"
      continue
    fi

    echo "running" > "$STATE_FILE"

    RESPONSE="$(
      python3 - <<'PY' | curl -sS "https://aiplatform.googleapis.com/v1/publishers/google/models/${MODEL}:generateContent?key=${GOOGLE_API_KEY}" \
        -X POST \
        -H "Content-Type: application/json" \
        --data-binary @-
import json
import os
import sys
prompt = os.environ["PROMPT_PAYLOAD"]
print(json.dumps({
  "contents": [
    {
      "role": "user",
      "parts": [
        {"text": prompt}
      ]
    }
  ]
}, ensure_ascii=False))
PY
    )"

    RESULT="$(python3 - <<'PY' "$RESPONSE"
import json,sys
raw=json.loads(sys.argv[1])
parts=raw["candidates"][0]["content"]["parts"]
texts=[p.get("text","") for p in parts if "text" in p]
print("\n".join(texts).strip())
PY
)"
    printf '%s\n' "$RESULT" > "$RESULT_FILE"
    echo "processed" > "$STATE_FILE"
    rm -f "$TASK_FILE"
  done

  sleep 3
done
EOF
chmod +x scripts/worker_gemini_loop.sh

上面这版是按跑通逻辑整理的最小可复现实现。它和会话中实际跑通的 worker 结构一致:轮询 tasks/*.json,写 state/*.stateoutputs/*.txt,并使用 Vertex Gemini 返回文本结果。已知后续测试中 state 会先显示 running,完成后变成 processedresult 则返回最终文本。

start_gemini_worker.sh

cd ~/agn-lab && cat > scripts/start_gemini_worker.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
export GOOGLE_API_KEY="${GOOGLE_API_KEY:?GOOGLE_API_KEY is not set}"
cd "$HOME/agn-lab"
exec ./scripts/worker_gemini_loop.sh >> "$HOME/agn-lab/logs/worker_gemini_loop.stdout.log" 2>> "$HOME/agn-lab/logs/worker_gemini_loop.stderr.log"
EOF
chmod +x scripts/start_gemini_worker.sh

6. 先用 tmux 启动后台 worker

在 VM SSH 中执行:

tmux kill-session -t gemini-worker 2>/dev/null || true
tmux new-session -d -s gemini-worker "export GOOGLE_API_KEY='<GOOGLE_API_KEY>'; export PROMPT_PAYLOAD=''; cd ~/agn-lab; ./scripts/start_gemini_worker.sh"

检查:

tmux ls

手工丢一个测试任务:

cd ~/agn-lab && printf '{"task_id":"t003","model":"gemini-3.1-pro-preview","prompt":"Explain in 3 concise lines what tmux is useful for on a remote Linux VM."}\n' > tasks/t003.json

检查结果:

cd ~/agn-lab && cat outputs/t003.txt && echo && cat state/t003.state && echo && tail -n 20 logs/worker_gemini_loop.stdout.log

随后会话里又把任务边界升级成 task_id + model + prompt,并保持 worker 仍只调 Gemini。


7. 验证 JSON 接口和 worker 链路

在 VM SSH 中执行:

cd ~/agn-lab && ./scripts/task_api_json.sh submit t013 gemini-3.1-pro-preview "Explain in 2 concise sentences what a systemd service is."
cd ~/agn-lab && sleep 8 && ./scripts/task_api_json.sh state t013 && echo && ./scripts/task_api_json.sh result t013

已知实际现象是:

  • 第一次可能返回 running
  • 结果字段可能先是 "running"
  • 再等一轮后状态变成 processed
  • result 返回模型最终文本

这说明常驻执行层已经成立。


8. 把 Gemini worker 托管成 systemd 服务

确认 tmux 验证通过后,再把 worker 提升为系统服务。

写环境文件:

sudo mkdir -p /etc/agn && sudo chmod 755 /etc/agn && sudo sh -c 'printf "%s\n" "GOOGLE_API_KEY=<GOOGLE_API_KEY>" > /etc/agn/gemini.env' && sudo chmod 600 /etc/agn/gemini.env

停掉 tmux worker:

tmux kill-session -t gemini-worker 2>/dev/null || true

创建 systemd service:

sudo tee /etc/systemd/system/agn-gemini-worker.service >/dev/null <<'EOF'
[Unit]
Description=AGN Gemini Loop Worker
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=<VM_USER>
WorkingDirectory=/home/<VM_USER>/agn-lab
EnvironmentFile=/etc/agn/gemini.env
ExecStart=/home/<VM_USER>/agn-lab/scripts/worker_gemini_loop.sh
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
EOF

启动并启用:

sudo systemctl daemon-reload && sudo systemctl enable --now agn-gemini-worker

检查:

systemctl status agn-gemini-worker --no-pager
journalctl -u agn-gemini-worker -n 50 --no-pager

再丢一次测试任务:

cd ~/agn-lab && ./scripts/task_api_json.sh submit t013 gemini-3.1-pro-preview "Explain in 2 concise sentences what a systemd service is."
cd ~/agn-lab && sleep 8 && ./scripts/task_api_json.sh state t013 && echo && ./scripts/task_api_json.sh result t013

文件里的实际结果显示该服务处于 active (running),之后测试任务能正常流转。


9. 在本地 Mac 建远端调用脚本

这一步开始,远端 executor 不再依赖你本地的 tmux 会话,本地只负责通过 SSH 触发稳定接口。

在 macOS 本地 Terminal 中执行:

mkdir -p ~/agn-client && cat > ~/agn-client/agn_remote.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

HOST="<VM_USER>@<VM_IP>"
ACTION="${1:?missing action}"

case "$ACTION" in
  submit)
    TASK_ID="${2:?missing task_id}"
    PROMPT="${3:?missing prompt}"
    ssh "$HOST" "~/agn-lab/scripts/task_api_json.sh submit $TASK_ID gemini-3.1-pro-preview \"$PROMPT\""
    ;;
  state)
    TASK_ID="${2:?missing task_id}"
    ssh "$HOST" "~/agn-lab/scripts/task_api_json.sh state $TASK_ID"
    ;;
  result)
    TASK_ID="${2:?missing task_id}"
    ssh "$HOST" "~/agn-lab/scripts/task_api_json.sh result $TASK_ID"
    ;;
  wait)
    TASK_ID="${2:?missing task_id}"
    while true; do
      STATE="$(ssh "$HOST" "~/agn-lab/scripts/task_api_json.sh state $TASK_ID" | python3 -c 'import sys,json; print(json.load(sys.stdin)["state"])')"
      if [ "$STATE" = "processed" ]; then
        ssh "$HOST" "~/agn-lab/scripts/task_api_json.sh result $TASK_ID"
        exit 0
      fi
      if [ "$STATE" = "failed" ]; then
        echo '{"ok": false, "error": "failed"}'
        exit 1
      fi
      sleep 2
    done
    ;;
  run)
    TASK_ID="${2:?missing task_id}"
    PROMPT="${3:?missing prompt}"
    ssh "$HOST" "~/agn-lab/scripts/task_api_json.sh submit $TASK_ID gemini-3.1-pro-preview \"$PROMPT\"" >/dev/null
    while true; do
      STATE="$(ssh "$HOST" "~/agn-lab/scripts/task_api_json.sh state $TASK_ID" | python3 -c 'import sys,json; print(json.load(sys.stdin)["state"])')"
      if [ "$STATE" = "processed" ]; then
        ssh "$HOST" "~/agn-lab/scripts/task_api_json.sh result $TASK_ID"
        exit 0
      fi
      if [ "$STATE" = "failed" ]; then
        echo '{"ok": false, "error": "failed"}'
        exit 1
      fi
      sleep 2
    done
    ;;
  *)
    echo "invalid_action"
    exit 1
    ;;
esac
EOF
chmod +x ~/agn-client/agn_remote.sh

测试:

~/agn-client/agn_remote.sh run t015 "Explain in 2 concise sentences what an SSH-based Gemini executor is."

这时本地已经有一个统一入口去驱动远端 VM。


10. 给 OpenClaw 准备本地包装层

在 macOS 本地 Terminal 中执行:

mkdir -p ~/agn-client && cat > ~/agn-client/openclaw_executor.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

TASK_ID="${1:?missing task_id}"
PROMPT="${2:?missing prompt}"

exec "$HOME/agn-client/agn_remote.sh" run "$TASK_ID" "$PROMPT"
EOF
chmod +x ~/agn-client/openclaw_executor.sh

再加一层 JSON 包装:

cat > ~/agn-client/openclaw_executor_json.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

TASK_ID="${1:?missing task_id}"
PROMPT="${2:?missing prompt}"

RESULT="$("$HOME/agn-client/agn_remote.sh" run "$TASK_ID" "$PROMPT")"
python3 -c 'import json,sys; raw=json.loads(sys.argv[1]); print(json.dumps({"ok":True,"task_id":sys.argv[2],"backend":"gemini-vm","result":raw["result"]}, ensure_ascii=False))' "$RESULT" "$TASK_ID"
EOF
chmod +x ~/agn-client/openclaw_executor_json.sh

测试:

~/agn-client/openclaw_executor_json.sh oc002 "Explain in 2 concise sentences what SSH-based orchestration provides."

这一层的作用很单纯:把 OpenClaw 看到的接口稳定下来,不让它知道远端 VM 内部目录结构,也不让它直接碰 Gemini API。


11. 在远端 VM 上安装 OpenClaw

在 VM SSH 中执行:

sudo apt update && sudo apt -y install curl ca-certificates && curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt -y install nodejs && sudo npm install -g openclaw@latest && node -v && npm -v && openclaw --version

文件里的实际版本检查通过,Node、npm、OpenClaw 都成功安装。


12. 运行 OpenClaw onboarding

在 VM SSH 中执行:

openclaw onboard --install-daemon

这一步是交互式向导。会话里确认它在 Linux 上会安装成 daemon 路径,接下来通过 TUI 完成 provider 配置即可。实际记录里,安装后继续推进到了远端 Rust 验证任务。

这里 provider 配置请直接填你自己的值:

  • email: <YOUR_EMAIL>
  • API key: <OPENAI_API_KEY>

不要把真实 credential 写进脚本或博客正文。


13. 用 OpenClaw 验证远端 Rust 执行能力

启动 TUI 或 gateway 后,直接给 agent 发送下面这个任务:

Your task is to prove that you can independently complete a real Rust task on this Ubuntu machine and return the artifacts.

Do the following exactly:

1. Create a new directory at:
~/agn-rust-validation/task001

2. Inside it, create a new Rust project:
cargo new hello_agent

3. Modify src/main.rs so that running the program prints exactly:
hello from Cordex on remote Ubuntu

4. Run:
cargo run

5. Create a file:
~/agn-rust-validation/task001/REPORT.md

In REPORT.md include:
- the absolute project path
- whether cargo run succeeded
- the exact stdout output
- the full contents of Cargo.toml
- the full contents of src/main.rs

6. Reply to me with:
- a brief summary of what you did
- the exact absolute path of REPORT.md
- the full contents of REPORT.md

Rules:
- Do the work directly on this machine.
- Do not just suggest commands.
- Actually execute them.
- If anything fails, diagnose it, fix it, and continue until the task is completed.

这是整篇文章里最关键的一步。已知最终结果是:

  • 远端 Ubuntu 上确实创建了 Rust 项目
  • 修改了 src/main.rs
  • cargo run 成功
  • 生成了 REPORT.md
  • 本地可以把这些产物拉回来看

也就是说,远端 agent 已经不是“会建议命令”,而是“在 VM 上实际执行任务并回传产物”。


14. 从远端拉回产物

在 macOS 本地 Terminal 中执行:

mkdir -p ~/Downloads/agn-rust-validation-task001 && \
scp <VM_USER>@<VM_IP>:/home/<VM_USER>/agn-rust-validation/task001/REPORT.md ~/Downloads/agn-rust-validation-task001/ && \
scp <VM_USER>@<VM_IP>:/home/<VM_USER>/agn-rust-validation/task001/hello_agent/Cargo.toml ~/Downloads/agn-rust-validation-task001/ && \
scp <VM_USER>@<VM_IP>:/home/<VM_USER>/agn-rust-validation/task001/hello_agent/src/main.rs ~/Downloads/agn-rust-validation-task001/

如果只想直接看报告内容:

ssh <VM_USER>@<VM_IP> 'cat /home/<VM_USER>/agn-rust-validation/task001/REPORT.md'

这一步必须在本地 Mac 上执行,不是在远端 SSH 里执行。文件里这一点专门被反复确认过。


15. 已经跑通的链路

到这里,这条链路已经能完成:

  1. VM 上安装 Rust、Node、OpenClaw
  2. VM 上验证 tmux、Rust 构建链、文件任务流
  3. VM 上建立 Gemini worker 与任务 API
  4. 本地 Mac 通过 SSH 成功调用远端任务接口
  5. VM 上安装并配置 OpenClaw
  6. OpenClaw 在远端 Ubuntu 上独立创建 Rust 项目、修改源码、运行 cargo run
  7. Rust 产物与 REPORT.md 成功回传并可被本地拉取

阶段2

到上部分为止,AGN 的远端执行链路已经完全跑通:Telegram 进消息,OpenClaw 在 VM 上执行任务,Rust 产物成功回传。主链稳定,走的是 OpenAI Codex 5.1 Mini。

但我还想把 Google Vertex AI 也接进来——GCP 有免费额度,Gemini 2.5 Flash/Pro 作为备选 provider 可以降低对单一供应商的依赖。

15. 调查 OpenClaw 的 google-vertex 现状

第一步不是动手,是调查。

OpenClaw v2026.3.13 文档里确实列出了 google-vertex 作为 built-in provider,声称支持 gcloud ADC 认证。但实际跑起来会遇到两个阻塞性问题:

问题一:认证检测逻辑错误。 OpenClaw 的 model-auth.ts 里,google-vertex 的认证路径调用的是 getEnvApiKey()——这个函数在找 API key 格式的环境变量,而 ADC 凭据根本不是 API key。凭据文件明明存在(~/.config/gcloud/application_default_credentials.json),但函数拿不到它期望的格式,直接返回 null,报 “No API key found”。这个 bug(Issue #11413)从 2026 年 2 月就被报了,至今仍然 Open。

问题二:即使绕过认证,运行时也会崩。 Issue #33392 报告了另一个问题:就算 openclaw models status --probe 显示认证通过,实际发起对话时会在 streamFn 创建阶段崩溃(Cannot convert undefined or null to object)。同样 Open。

v2026.3.13 的 release notes 里唯一和 google-vertex 相关的修复是 model ID 规范化(PR #42435),和这两个问题完全无关。

结论:不是 Vertex 不可用,不是 Google 不支持,是 OpenClaw 的集成实现有缺陷。绕开它。

16. 决策:自建 Vertex Proxy

既然 OpenClaw 内部的 google-vertex 路径走不通,那就在 OpenClaw 之外建一个独立的代理层。设计原则:

  • 最小化:一个 FastAPI 进程,三个文件
  • OpenAI 兼容:暴露 /v1/chat/completions 接口,让 AGN 的 model_router 能直接调
  • 只做透传:认证(ADC)在代理层处理,AGN 不需要知道 GCP 的细节
  • 不碰主链:作为独立 provider 注册,但不进入自动路由

技术栈选了 Python + FastAPI + google-genai SDK。原因很简单:最快。Google 的官方 SDK 对 ADC 的支持是完整的,不需要任何 workaround。

核心代码不到 150 行:

from google import genai
from google.genai import types

# Vertex 模式,ADC 认证,一行搞定
client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)

# 收到请求后直接透传给 Gemini
response = client.models.generate_content(
    model=model_id,
    contents=contents,
    config=gen_config,
)

代理支持非流式和 SSE 流式两种模式,支持 gemini-2.5-flashgemini-2.5-pro 双 model 切换。用 systemd 做守护,只监听 127.0.0.1:8099,不暴露到公网。

17. 注册进 AGN:可见但不上主链

代理跑起来之后,要让 AGN 的调度系统能认识它。这涉及两个配置文件:

config/providers.json 里注册 vertex_local 作为 executor 和 reviewer:

"vertex_local": {
  "kind": "api",
  "default_base_url": "http://127.0.0.1:8099/v1",
  "default_model": "gemini-2.5-flash",
  "requires_api_key": false
}

config/model_router.json 里注册路由策略,但关键是:不加进 default_provider_order。这意味着 model_router 在自动选择 provider 时永远不会选到它,只有手动指定 --force-provider vertex_local 才会走这条路。

"vertex_local": {
  "max_risk": "medium",
  "max_complexity": "high",
  "cost_tier": "free_local",
  "allowed_profiles": [
    "structured_transform", "json_extraction",
    "bounded_summarization", "general_analysis", "review"
  ]
}

注意 complex_reasoning 不在 allowed_profiles 里——因为代理当前不支持 tools/function calling,复杂推理任务不适合走这条路。

18. Patch model_router:支持"旁路 provider"

注册完之后跑验证,发现 --force-provider vertex_local 不生效。原因是 model_router.py 的 forced_provider 逻辑:

if forced_provider:
    chain = [item for item in chain if item["provider"] == forced_provider]

它从 build_route_decision 返回的 candidate_chain 里过滤——但 vertex_local 从来没进过 chain(因为不在 default_provider_order 里),过滤完就是空列表。

修复逻辑:当 forced_provider 存在于 provider_policies 但不在 candidate_chain 里时,构造一个合成候选项注入:

if forced_provider:
    chain = [item for item in chain if item["provider"] == forced_provider]
    if not chain:
        fp_policy = conf.get("provider_policies", {}).get(forced_provider)
        if fp_policy:
            chain = [{"provider": forced_provider,
                       "timeout_sec": fp_policy.get("timeout_sec", 120.0),
                       "retry_count": fp_policy.get("retry_count", 0),
                       "model_name": ""}]

这个 patch 解决的是一个通用问题:怎么让路由系统支持"注册但不自动参与选举"的 provider。 在任何多 provider 架构里,你都会遇到需要某些 provider 只能被显式调用的场景——实验性模型、成本敏感的高端模型、或者像这里一样的旁路代理。

19. 全链路验证

echo '{"prompt":"Reply with exactly: AGN_VERTEX_ROUTE_OK","profile":"bounded_summarization","risk":"low"}' \
  | python3 scripts/model_router.py run --from-stdin --force-provider vertex_local

返回:

{
  "ok": true,
  "route_decision": {
    "selected_provider": "vertex_local",
    "candidate_chain": [{"provider": "vertex_local", "timeout_sec": 120.0}]
  },
  "result": {
    "content": "AGN_VERTEX_ROUTE_OK",
    "duration_ms": 1019.95,
    "usage": {"prompt_tokens": 80, "completion_tokens": 7, "total_tokens": 87}
  }
}

从 AGN 的 model_router 出发,经过 forced_provider 路由,打到本地 Vertex Proxy,透传到 GCP Vertex AI,Gemini 2.5 Flash 返回结果,整条链路 1 秒内完成。主链没有受到任何影响。


阶段六:多 Agent 协作——不是概念,是工程实践

整个部署过程中有一个值得单独拿出来说的维度:这不是一个人在干活。

20. 三方协作模式

这次 session 里实际参与的角色:

  • Archiver(GPT):负责前期的 VM 搭建策略、最小闭环验证方法论、以及从系统行为层面判断 OpenClaw google-vertex 的问题。它先从外部行为得出"不是 Vertex 坏,是 OpenClaw 这条实现坏"的结论。
  • Navigator(Claude):负责代码层面的调查(定位到 model-auth.ts 的具体缺陷)、Vertex Proxy 的实现、model_router patch、以及 AGN 集成的配置设计。
  • Alex:在两者之间做交叉验证和最终决策。把 Archiver 的宏观判断拿来和 Navigator 的代码层发现对照,把 Navigator 的实现方案拿回去让 Archiver 确认方向。

信息流不是单线的——它是三角形的。Archiver 先从架构层给出方向,Navigator 再往下落到实现层,Alex 在中间确保两边的判断一致。

21. Cordex:云端 Coordinator 的角色定义

AGN 云端实例跑起来之后,还需要一个 Coordinator 来管理日常的任务编排。这个角色交给了另一个 GPT 实例(代号 Cordex),它的职责边界被写进了一份正式的 briefing doc:

  • 可以通过正常 pipeline 调度任务
  • 可以使用 --force-provider vertex_local 做实验性调用
  • 不能修改 default_provider_order(主链)
  • 不能把 vertex_local 提升为自动 fallback
  • 任何涉及 git push、配置变更的操作必须经过 Alex 确认

这不是形式主义——它本质上是一种治理约束的文档化。当你的系统有多个 agent 在运作时,明确每个角色能做什么、不能做什么,比事后排查谁改了什么要高效得多。

22. 基础设施工具:agn-health

作为 Cordex 接手后的第一个实际任务,我们让主链(Codex 5.1 Mini)写一个 Rust 命令行工具 agn-health:读取一个 JSON 配置文件,并发请求所有 endpoint,输出健康状态表格。这个工具之后会成为 AGN 基础设施监控的一部分。

这个任务本身不复杂,但它验证了两件事:

  1. 主链的 Rust 执行能力在云端是完整可用的
  2. Cordex 作为 Coordinator 能正确地通过 AGN pipeline 下发和追踪任务

当前系统状态

到这里,agn-vm-01 上的能力清单:

组件状态说明
OpenClaw GatewayOpenAI Codex 5.1 Mini,主链
Telegram Bot消息收发通道
Brave SearchWeb 搜索工具
Rust Executioncargo build/run/test
Vertex ProxyGemini 2.5 Flash/Pro,手动路由
AGN Pipelinedispatch → execute → review 全链路

主链架构:

Telegram → coordinator → dispatcher → model_router → [Codex via OpenClaw] → executor → reviewer → Telegram

Vertex 旁路:

手动 dispatch → model_router(--force-provider) → vertex_local → 127.0.0.1:8099 → GCP Vertex AI → Gemini 2.5

复盘

回头看这次 session,有几个决策点值得记录:

“先验证再抽象”。 每一步都是先跑通最小闭环,确认产物存在(文件被创建了、编译通过了、API 返回了正确结果),然后才往上叠加。不是先设计完美架构再动手。

“主链不动”。 从头到尾,OpenAI 主链没有被任何实验性改动影响过。新 provider 通过旁路接入,注册但不进自动路由。这不是保守,是工程化的风险控制。

“坏了就绕”。 OpenClaw 的 google-vertex 集成不可用,花了 15 分钟调查确认问题在 OpenClaw 而不是 Vertex 本身,然后果断放弃修 OpenClaw、转向自建代理。27 分钟后全链路通过。在时间有限的情况下,识别沉没成本并切换方向比执着于修复别人的 bug 更有价值。

“多 Agent 不是噱头”。 三个不同的 AI agent 在同一个 session 里分别负责不同抽象层级的工作(架构判断、代码实现、任务编排),通过一个人类决策者做交叉验证。这个模式确实有效——前提是每个角色的边界和职责被清晰定义。