📎 配套脚本附件正文未内嵌 · 共 15 个 · 点开可在线查看
📚 另需公共库(正文未重复内嵌,点名字直接看):_agent_cfg.py、_bootstrap.py、_config.py、_plugin_util.py
📂 查看全部 761 个脚本附件 →
⚠️ 脚本内含明文凭据(与本页正文一致),请勿在公网环境直接运行。
实战作业

作业 1 · 带 HTTP 工具的 Agent

理论流程 + 完整代码 + CPU/GPU 建议 + 实战踩坑:查询服务器状态工具调用。

作业 1:创建一个带工具调用的 Agent(基础 · 查询服务器状态)

作业目标:在已有 Dify + vLLM + Langfuse 链路上,搭建第一个会调 HTTP 工具的新 Agent。

验收标准:问 Agent「帮我看看 gpu-server-01 的状态」→ 调用工具 getServerStatus → 返回格式化的 CPU / 内存 / GPU 指标。

环境:192.168.110.207(Dify 1.16.1 + FastAPI 模拟 API)+ 116.62.10.183(vLLM Qwen3.6-27B)。

HTML 成品:docs/html_cursor/作业1_带工具调用的Agent_服务器状态_cursor.html

运行环境:业务脚本均在 207 固定目录 /opt/workbuddy 执行,Python 为 /usr/bin/python3。 Windows 开发机仅保留 _probe_* 调试脚本;同步命令:python scripts_cursor/完整代码约定/00_部署_workbuddy到207.py。


目录

  1. 做完长什么样
  2. 第一部分 原理:工具调用 Agent 怎么跑
  3. 第二部分 前置条件
  4. 第三部分 详细操作步骤
  5. 第四部分 CPU 与 GPU 在本作业中的分工(当前 + 未来)
  6. 第五部分 完整代码程序
  7. 第六部分 本次实战问题与解决方案
  8. 第七部分 对本系统的专业建议
  9. 第八部分 验收清单

做完长什么样

项本环境实测值
模拟 APIhttp://192.168.110.207:8777/server/status
OpenAPI Providerserver_status(UUID 见 _config.py)
工具 operationIdgetServerStatus
Agent 名称运维巡检 Agent
app_idd00d0382-ca3f-4cf2-9cc9-0cb5aed9404d
agent_id01a0189c-7d7e-742f-83d1-74c5c16878da
验收问句帮我看看 gpu-server-01 的状态

控制台:http://192.168.110.207 → 运维巡检 Agent → 新开对话 → 发验收句。

流式响应中应出现:


第一部分 原理:工具调用 Agent 怎么跑

1.1 与「纯知识库 Agent」的区别

知识库 Agent(行政问答)本作业 Agent(HTTP 工具)
能力来源检索制度文档调用外部 REST API
中间层Embedding + Weaviate + RerankDify OpenAPI 自定义工具
模型行为调 knowledge_base_search调 getServerStatus
数据真实性来自知识库片段来自 HTTP 返回 JSON

本作业不绑知识库,只验证「模型 → 工具 → 外部 API → 再生成」闭环。

1.2 完整执行流程(逐步)

text
【0】用户在 Dify 工作室 / API 发问
      query = "帮我看看 gpu-server-01 的状态"
      conversation_id = ""(必须新开对话)
        │
        ▼
【1】docker-api-1 接收(完整 URL,agent_id 为本环境实测值)
      POST http://127.0.0.1/console/api/agent/01a0189c-7d7e-742f-83d1-74c5c16878da/chat-messages
      读取 agent_config_snapshots:
        - model → qwen3.6-27b
        - prompt.system_prompt → 要求必须调 getServerStatus
        - tools.dify_tools[] → API 工具元数据
        │
        ▼
【2】docker-agent_backend-1 组装 Agent 运行请求
      把 OpenAPI 工具 schema 转成 LLM 可见的 tools 定义
        │
        ▼
【3】第一次 LLM(116 vLLM · Qwen3.6-27B)
      模型输出 tool_call:getServerStatus(server_name="gpu-server-01")
      ※ 依赖 vLLM --tool-call-parser qwen3_xml(专题四/九)
        │
        ▼
【4】Dify 执行 HTTP 工具
      经 ssrf_proxy 出网
      GET http://192.168.110.207:8777/server/status?server_name=gpu-server-01
        │
        ▼
【5】FastAPI 模拟服务(207 CPU)
      返回 JSON:cpu/memory/gpu_util/gpu_memory
        │
        ▼
【6】第二次 LLM
      把 tool observation 塞进上下文,按 system_prompt 格式化中文汇报
        │
        ▼
【7】写 messages 表,SSE 推流给前端
      可选:Celery → Langfuse Trace

1.3 配置落在哪(三层)

text
工作区「集成 → 工具」          ← 注册 OpenAPI Provider(server_status)
        │
Agent 快照 tools.dify_tools[]  ← 对话真正挂载哪些工具(provider_id 必须是 UUID)
        │
Agent 快照 prompt / model      ← 会不会调工具、用什么 LLM

改快照必须同时改草稿(agent_config_snapshots + agent_config_drafts)。改完 不要点「构建 / 发布」(专题十一)。

1.4 Dify 1.16 新 Agent 创建口(易错)

方式结果
POST http://127.0.0.1/console/api/apps + "mode":"agent"❌ 400,CreateAppPayload 不允许 agent
POST http://127.0.0.1/console/api/agent✅ 正确,返回 app_id / agent_id

第二部分 前置条件

#条件怎么确认
1Dify 1.16 已跑,能登录控制台http://192.168.110.207
2vLLM Qwen3.6-27B 可用curl -H "Authorization: Bearer 123456" http://116.62.10.183:8000/v1/models
3vLLM 已开工具解析--tool-call-parser qwen3_xml(不是 qwen3)
4Dify 已配 Qwen 凭据工作区模型供应商
5207 上 8777 端口未被占用`ss -tlnp \
6207 上已部署 /opt/workbuddy/opt/workbuddy/dify_api.py + /opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/_config.py

一键前置检查(可选):/usr/bin/python3 /opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/专题十五_00_创建前检查.py


第三部分 详细操作步骤

推荐:按 3.1~3.5 顺序跑完整 Python 脚本(在 207 上执行)。

等效 UI:每步附手工路径,API 搞不定时用 UI 补。

3.0 一键顺序(脚本)

bash
/usr/bin/python3 /opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_01_部署模拟服务.py
/usr/bin/python3 /opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_02_注册OpenAPI工具.py
/usr/bin/python3 /opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_03_创建Agent空壳.py
/usr/bin/python3 /opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_04_查ID并挂工具.py
/usr/bin/python3 /opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_05_对话验收.py

期望最后一行:[PASS] Agent 已调用工具并返回状态信息。

换环境只改 /opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/_config.py 中 SERVER_STATUS_*、HOMEWORK1_*。

3.0.1 全局图景(先读这个,再往下翻 curl 细节)

阶段脚本目的
部署01 → 04FastAPI 模拟服务器 + Dify OpenAPI + Agent 挂工具
验收05问「gpu-server-01 状态」并确认 Agent 调了 HTTP 工具
text
用户问「gpu-server-01 状态」
  → Dify Agent(207)
  → OpenAPI Provider「server_status」
  → FastAPI :8788(207 本机,模拟三台服务器 JSON)
  → Agent 汇总 CPU/内存/磁盘 回复

各脚本四要素速查(详细 curl 见下方 3.1~3.5):

脚本干什么触发什么成功标志典型失败
01_部署模拟服务启动 :8788 FastAPIuvicorn + curl 探针[OK] 模拟服务已启动8788 端口占用
02_注册OpenAPI注册 server_status providerDify POST add schemaHTTP 200yaml 与路由不一致
03_创建Agent空壳POST /agent 建「运维巡检 Agent」仅建空壳,无工具already exists / 201用错 /apps+agent 模式 400
04_查ID并挂工具写快照 tools + system_promptpsql UPDATE 快照/草稿[OK] 已挂工具model=null、UUID 空
05_对话验收自动发验收 queryAgent streaming + 工具调用[PASS] Agent 已调用工具Connection error、未调工具

下方 3.1~3.5 保留完整 curl 与代码引用;第六部分 有 13 条实战排错表。

控制台 API 完整命令(登录一次,后续接口都能打)

Dify 1.16 控制台接口必须:登录拿 cookie + X-CSRF-Token。密码字段是 base64(不是明文)。脚本 dify_api.py 会 在 207 本机打 http://127.0.0.1/console/api。

bash
ssh root@192.168.110.207
rm -f /tmp/dify_cj.txt
python3 - <<'PY'
import json, base64
open("/tmp/dify_login.json", "w").write(json.dumps({
    "email": "38205036@qq.com",
    "password": base64.b64encode(b"Difyai123456").decode(),
    "language": "zh-Hans",
    "remember_me": True,
}))
PY
curl -s -c /tmp/dify_cj.txt -X POST 'http://127.0.0.1/console/api/login' \
  -H 'Content-Type: application/json' \
  -d @/tmp/dify_login.json
CSRF=$(python3 - <<'PY'
jar=open("/tmp/dify_cj.txt").read()
for line in jar.splitlines():
    if "csrf_token" in line:
        print(line.split("\t")[-1].strip())
PY
)
echo "CSRF=$CSRF"

列出新 Agent(不要用 psql 拼中文名):

bash
curl -s -b /tmp/dify_cj.txt -H "X-CSRF-Token: $CSRF" \
  'http://127.0.0.1/console/api/agent?page=1&limit=50'

创建空壳(本作业名称「运维巡检 Agent」):

bash
curl -s -b /tmp/dify_cj.txt -c /tmp/dify_cj.txt \
  -H "X-CSRF-Token: $CSRF" -H 'Content-Type: application/json' \
  -X POST 'http://127.0.0.1/console/api/agent' \
  -d '{
    "name": "运维巡检 Agent",
    "description": "作业 1:带 HTTP 工具的运维巡检 Agent(查询服务器状态)",
    "icon_type": "emoji",
    "icon": "🖥️",
    "icon_background": "#E8F5E9",
    "role": "ops"
  }'

对话验收(本环境 agent_id):

bash
curl -s -b /tmp/dify_cj.txt -c /tmp/dify_cj.txt \
  -H "X-CSRF-Token: $CSRF" -H 'Content-Type: application/json' \
  --max-time 420 \
  -X POST 'http://127.0.0.1/console/api/agent/01a0189c-7d7e-742f-83d1-74c5c16878da/chat-messages' \
  -d '{
    "inputs": {},
    "query": "帮我看看 gpu-server-01 的状态",
    "response_mode": "streaming",
    "files": [],
    "conversation_id": ""
  }'

模拟 API(不需要登录,可在 Windows 或 207 上打):

bash
curl -s 'http://192.168.110.207:8777/health'
curl -s 'http://192.168.110.207:8777/server/status?server_name=gpu-server-01'
curl -H "Authorization: Bearer 123456" 'http://116.62.10.183:8000/v1/models'

3.1 步骤 1 · 部署 FastAPI 模拟服务(207 CPU)

理论

Agent 调用的不是 Dify 内置能力,而是宿主机上的 HTTP 服务。Dify 容器通过 ssrf_proxy 访问 192.168.110.207:8777,因此:

操作

方式 A(推荐):运行部署脚本(SFTP 上传 + 启动 + 探针)

完整程序:/opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_01_部署模拟服务.py

运行/引用:/usr/bin/python3 /opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_01_部署模拟服务.py

python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""作业 1 · 步骤 1:把 FastAPI 模拟服务部署到 207 并启动。"""
from __future__ import annotations

import sys
from pathlib import Path

import _bootstrap  # noqa: F401
import _config as C
from dify_api import Dify

REMOTE_DIR = "/opt/server_status_api"
LOCAL_API = Path(__file__).resolve().parent / "作业1_server_status_api.py"
LOCAL_SH = Path(__file__).resolve().parents[1] / "scripts_bash_cursor" / "作业1_启动_server_status.sh"


def main() -> int:
    d = Dify(C.DIFY_EMAIL, C.DIFY_PASSWORD)
    try:
        d.login()
        d.sh("mkdir -p %s" % REMOTE_DIR)
        d.sftp.put(str(LOCAL_API), REMOTE_DIR + "/作业1_server_status_api.py")
        d.sftp.put(str(LOCAL_SH), REMOTE_DIR + "/start.sh")
        d.sh("chmod +x %s/start.sh" % REMOTE_DIR)
        # 若旧进程占端口则先停
        d.sh(
            "if [[ -f /tmp/server_status_api.pid ]]; then "
            "kill $(cat /tmp/server_status_api.pid) 2>/dev/null || true; fi"
        )
        out, err = d.sh("bash %s/start.sh" % REMOTE_DIR)
        print(out or err)
        r = d.sh(
            "curl -s 'http://127.0.0.1:%d/server/status?server_name=gpu-server-01'"
            % C.SERVER_STATUS_PORT
        )
        print("probe:", r[0].strip())
        return 0 if "gpu-server-01" in (r[0] or "") else 1
    finally:
        d.close()


if __name__ == "__main__":
    sys.exit(main())

方式 B(207 上手工):

完整脚本:scripts_bash_cursor/作业1_带工具调用的Agent_服务器状态/作业1_启动_server_status.sh

运行/引用:bash /opt/workbuddy/scripts_bash_cursor/作业1_带工具调用的Agent_服务器状态/作业1_启动_server_status.sh

bash
#!/usr/bin/env bash
# 作业 1 · 在 207 上启动模拟「查询服务器状态」FastAPI 服务
set -euo pipefail

DIR="/opt/server_status_api"
PORT="${SERVER_STATUS_PORT:-8777}"
PIDFILE="/tmp/server_status_api.pid"
LOG="/tmp/server_status_api.log"

mkdir -p "$DIR"
pip3 install fastapi uvicorn -q

if [[ -f "$PIDFILE" ]] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
  echo "server_status_api already running pid=$(cat "$PIDFILE")"
  exit 0
fi

cd "$DIR"
nohup python3 -m uvicorn 作业1_server_status_api:app \
  --host 0.0.0.0 --port "$PORT" >>"$LOG" 2>&1 &
echo $! >"$PIDFILE"
sleep 1
curl -sf "http://127.0.0.1:${PORT}/health" && echo
echo "started on :${PORT}, log=$LOG"

探针

bash
curl -s 'http://192.168.110.207:8777/server/status?server_name=gpu-server-01'
curl -s 'http://192.168.110.207:8777/health'

期望:JSON 含 "cpu":"45%" 等字段。

完整 FastAPI 源码

完整程序:/opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_server_status_api.py

运行/引用:/usr/bin/python3 /opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_server_status_api.py

python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""作业 1 · 模拟「查询服务器状态」HTTP 工具(FastAPI)。

启动(207 上):
  pip3 install fastapi uvicorn -q
  uvicorn 作业1_server_status_api:app --host 0.0.0.0 --port 8777

或:bash /opt/workbuddy/scripts_bash_cursor/作业1_带工具调用的Agent_服务器状态/作业1_启动_server_status.sh
"""
from __future__ import annotations

from fastapi import FastAPI

app = FastAPI(
    title="Server Status API",
    version="1.0.0",
    description="Dify Agent 作业 1:模拟查询服务器 CPU / 内存 / GPU 状态",
)


@app.get("/server/status", operation_id="getServerStatus", summary="查询服务器状态")
def get_status(server_name: str) -> dict[str, str]:
    """根据 server_name 返回模拟监控指标(本服务不连真实监控)。"""
    return {
        "server": server_name,
        "cpu": "45%",
        "memory": "62%",
        "gpu_util": "78%",
        "gpu_memory": "18.2/24 GB",
    }


@app.get("/health")
def health() -> dict[str, str]:
    return {"status": "ok"}


if __name__ == "__main__":
    import uvicorn

    uvicorn.run("作业1_server_status_api:app", host="0.0.0.0", port=8777, reload=False)

3.2 步骤 2 · 在 Dify 注册 OpenAPI 自定义工具

理论

Dify 通过 OpenAPI 3.0 Schema 解析出工具名、参数、HTTP 方法和 URL。注册后写入表 tool_api_providers,工作区所有 Agent 可选用。

操作

方式 A(推荐):

完整程序:/opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_02_注册OpenAPI工具.py

运行/引用:/usr/bin/python3 /opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_02_注册OpenAPI工具.py

python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""作业 1 · 步骤 2:在 Dify 注册 OpenAPI 自定义工具(查询服务器状态)。"""
from __future__ import annotations

import sys
from pathlib import Path

import _bootstrap  # noqa: F401
import _config as C
from dify_api import Dify, pp

SCHEMA_PATH = (
    Path(__file__).resolve().parents[1]
    / "patches_cursor"
    / "作业1_server_status_openapi.yaml"
)


def main() -> int:
    schema = SCHEMA_PATH.read_text(encoding="utf-8")
    d = Dify(C.DIFY_EMAIL, C.DIFY_PASSWORD)
    try:
        d.login()
        # 若已存在则先删再建,保证作业可重复跑
        existing = d.get("/workspaces/current/tools/api")
        names = []
        for item in existing.get("json") or []:
            names.append(item.get("name") or item.get("provider"))
        print("existing api tools:", names)
        if C.SERVER_STATUS_PROVIDER in names:
            r_del = d.post(
                "/workspaces/current/tool-provider/api/delete",
                body={"provider": C.SERVER_STATUS_PROVIDER},
            )
            pp("delete old", r_del)

        body = {
            "provider": C.SERVER_STATUS_PROVIDER,
            "credentials": {"auth_type": "none"},
            "schema_type": "openapi",
            "schema": schema,
            "icon": {"background": "#E8F5E9", "content": "🖥️"},
            "privacy_policy": "",
            "custom_disclaimer": "",
            "labels": ["ops", "homework1"],
        }
        r = d.post("/workspaces/current/tool-provider/api/add", body=body)
        pp("add api tool", r)
        if r.get("code") not in ("200", "201"):
            return 1

        r2 = d.get(
            "/workspaces/current/tool-provider/api/tools?provider=%s"
            % C.SERVER_STATUS_PROVIDER
        )
        pp("list tools", r2, limit=2000)
        r3 = d.get("/workspaces/current/tools/api")
        print("api tools now:", [
            (x.get("name") or x.get("provider")) for x in (r3.get("json") or [])
        ])
        return 0
    finally:
        d.close()


if __name__ == "__main__":
    sys.exit(main())

方式 B(UI):

  1. 控制台 → 集成 → 工具 → 创建自定义工具
  2. 名称:server_status
  3. 粘贴 OpenAPI(见下方完整 yaml)
  4. 鉴权:无
  5. 保存后在工具列表能看到 getServerStatus

完整 OpenAPI Schema

完整补丁:patches_cursor/作业1_带工具调用的Agent_服务器状态/作业1_server_status_openapi.yaml

运行/引用:见 patches_cursor/作业1_带工具调用的Agent_服务器状态/作业1_server_status_openapi.yaml

python
openapi: 3.0.0
info:
  title: Server Status API
  version: 1.0.0
  description: 作业 1 · 模拟查询服务器 CPU / 内存 / GPU 状态
servers:
  - url: http://192.168.110.207:8777
paths:
  /server/status:
    get:
      operationId: getServerStatus
      summary: 查询服务器状态
      description: 根据服务器名称返回 CPU、内存、GPU 利用率等模拟指标
      parameters:
        - name: server_name
          in: query
          required: true
          description: 服务器名称,例如 gpu-server-01
          schema:
            type: string
      responses:
        "200":
          description: 成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  server:
                    type: string
                  cpu:
                    type: string
                  memory:
                    type: string
                  gpu_util:
                    type: string
                  gpu_memory:
                    type: string

API 等价调用(207 上,先完成上面的登录拿 $CSRF)

bash
# 注册 OpenAPI 工具:schema 很长,务必用脚本上传 yaml,不要手贴。
# 脚本内部等价于:
# POST http://127.0.0.1/console/api/workspaces/current/tool-provider/api/add
/usr/bin/python3 /opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_02_注册OpenAPI工具.py

# 列出该 provider 下的 tools
curl -s -b /tmp/dify_cj.txt -H "X-CSRF-Token: $CSRF" \
  'http://127.0.0.1/console/api/workspaces/current/tool-provider/api/tools?provider=server_status'

# 列出工作区全部 API 工具
curl -s -b /tmp/dify_cj.txt -H "X-CSRF-Token: $CSRF" \
  'http://127.0.0.1/console/api/workspaces/current/tools/api'

3.3 步骤 3 · 创建新 Agent 空壳

理论

空壳只创建 App + Agent + 默认快照,尚未挂工具和写 Prompt。Dify 1.16 必须用 Roster API。

操作

完整程序:/opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_03_创建Agent空壳.py

运行/引用:/usr/bin/python3 /opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_03_创建Agent空壳.py

python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""作业 1 · 步骤 3:创建带工具调用的 Agent 空壳(mode=agent)。"""
from __future__ import annotations

import sys

import _bootstrap  # noqa: F401
import _config as C
from dify_api import Dify, pp


def main() -> int:
    d = Dify(C.DIFY_EMAIL, C.DIFY_PASSWORD)
    try:
        d.login()
        # 若同名已存在,打印 ID 供后续步骤用,不重复创建
        r_list = d.get("/agent?page=1&limit=50")
        data = (r_list.get("json") or {}).get("data") or []
        for item in data:
            if item.get("name") == C.HOMEWORK1_APP_NAME:
                print("already exists app_id=", item.get("id"), "agent_id=", item.get("agent_id") or item.get("id"))
                print("下一步:运行 作业1_04_查ID并挂工具.py")
                return 0

        # Dify 1.16 新 Agent 用 POST /agent(不是 /apps + mode=agent)
        body = {
            "name": C.HOMEWORK1_APP_NAME,
            "description": "作业 1:带 HTTP 工具的运维巡检 Agent(查询服务器状态)",
            "icon_type": "emoji",
            "icon": "🖥️",
            "icon_background": "#E8F5E9",
            "role": "ops",
        }
        r = d.post("/agent", body)
        pp("create agent", r)
        j = r.get("json") or {}
        print("app_id", j.get("id"))
        print("agent_id", j.get("agent_id") or j.get("id"))
        if r.get("code") not in ("200", "201"):
            return 1
        print("下一步:运行 作业1_04_查ID并挂工具.py")
        return 0
    finally:
        d.close()


if __name__ == "__main__":
    sys.exit(main())

UI 等价:控制台 → Agent → 创建 Agent → 名称「运维巡检 Agent」。

记录返回的 app_id、agent_id(或跑步骤 4 自动查)。


3.4 步骤 4 · 挂工具 + System Prompt + 模型

理论

新创建的空壳常见问题:

本步写入快照的 tools.dify_tools[0] 结构:

json
{
  "enabled": true,
  "provider_type": "api",
  "provider_id": "021a4445-098d-4821-81fe-ff0ebc660ecc",
  "tool_name": "getServerStatus",
  "credential_type": "unauthorized",
  "credential_ref": null
}

注意:provider_id 是 tool_api_providers.id(UUID),不是名称 server_status。

操作

完整程序:/opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_04_查ID并挂工具.py

运行/引用:/usr/bin/python3 /opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_04_查ID并挂工具.py

python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""作业 1 · 步骤 4:查 app/agent/snap/draft ID,挂工具,写提示词。"""
from __future__ import annotations

import json
import sys

import _bootstrap  # noqa: F401
import _agent_cfg as AC
import _config as C
from dify_api import Dify, pp

SYSTEM_PROMPT = """你是企业内部运维助手。当用户询问某台服务器的状态、CPU、内存、GPU 时,
必须调用工具 getServerStatus(参数 server_name),并根据工具返回结果用中文格式化汇报。

汇报格式示例:
- 服务器:{server}
- CPU:{cpu}
- 内存:{memory}
- GPU 利用率:{gpu_util}
- GPU 显存:{gpu_memory}

不要编造指标;工具失败时如实说明。"""


def find_ids(d: Dify) -> tuple[str, str, str, str]:
    # 列表 API 返回 UTF-8 正常;不要用中文名拼进远程 psql(Windows 编码易乱)
    r_list = d.get("/agent?page=1&limit=50")
    app_id = agent_id = ""
    for item in (r_list.get("json") or {}).get("data") or []:
        if item.get("name") == C.HOMEWORK1_APP_NAME:
            app_id = item.get("id") or ""
            agent_id = item.get("agent_id") or item.get("id") or ""
            break
    if not agent_id:
        raise RuntimeError("列表里找不到应用:%s" % C.HOMEWORK1_APP_NAME)

    sql = (
        "SELECT a.app_id, a.id, a.active_config_snapshot_id, d.id "
        "FROM agents a JOIN agent_config_drafts d ON d.agent_id=a.id "
        "WHERE a.id='%s' LIMIT 1;" % agent_id
    )
    out, err = d.sh(
        'docker exec %s psql -U postgres -d dify -t -A -F "|" -c "%s"'
        % (C.PG_CONTAINER, sql)
    )
    line = (out or "").strip().splitlines()
    if not line or "|" not in line[0]:
        # 有时列表的 id 就是 app_id
        sql2 = (
            "SELECT a.app_id, a.id, a.active_config_snapshot_id, d.id "
            "FROM agents a JOIN agent_config_drafts d ON d.agent_id=a.id "
            "WHERE a.app_id='%s' LIMIT 1;" % app_id
        )
        out, err = d.sh(
            'docker exec %s psql -U postgres -d dify -t -A -F "|" -c "%s"'
            % (C.PG_CONTAINER, sql2)
        )
        line = (out or "").strip().splitlines()
    if not line or "|" not in line[0]:
        raise RuntimeError("找不到快照/草稿: %s %s" % (out, err))
    app_id, agent_id, snap_id, draft_id = line[0].split("|")
    return app_id, agent_id, snap_id, draft_id


def resolve_tool_meta(d: Dify) -> dict:
    # provider_id 必须是 tool_api_providers.id(UUID),不能用 name
    out, err = d.sh(
        "docker exec %s psql -U postgres -d dify -t -A -c "
        "\"SELECT id FROM tool_api_providers WHERE name='%s' LIMIT 1;\""
        % (C.PG_CONTAINER, C.SERVER_STATUS_PROVIDER)
    )
    provider_uuid = (out or "").strip()
    if not provider_uuid:
        raise RuntimeError("找不到 tool_api_providers.name=%s: %s" % (C.SERVER_STATUS_PROVIDER, err))

    r = d.get(
        "/workspaces/current/tool-provider/api/tools?provider=%s"
        % C.SERVER_STATUS_PROVIDER
    )
    pp("api tools meta", r, limit=2500)
    data = r.get("json") or []
    if isinstance(data, dict):
        data = data.get("data") or data.get("tools") or [data]
    if not data:
        raise RuntimeError("未找到 provider=%s 的工具列表" % C.SERVER_STATUS_PROVIDER)
    tool = data[0]
    tool_name = tool.get("name") or tool.get("tool_name") or "getServerStatus"
    return {
        "enabled": True,
        "provider_type": "api",
        "provider_id": provider_uuid,
        "tool_name": tool_name,
        "credential_type": "unauthorized",
        "credential_ref": None,
        "runtime_parameters": {},
    }


def patch_cfg(cfg: dict, tool_entry: dict) -> dict:
    prompt = cfg.setdefault("prompt", {})
    if prompt is None:
        cfg["prompt"] = {}
        prompt = cfg["prompt"]
    prompt["system_prompt"] = SYSTEM_PROMPT

    tools = cfg.get("tools")
    if not isinstance(tools, dict):
        tools = {}
        cfg["tools"] = tools
    tools["cli_tools"] = tools.get("cli_tools") or []
    tools["dify_tools"] = [tool_entry]

    model = cfg.get("model")
    if not isinstance(model, dict):
        model = {}
        cfg["model"] = model
    model["model"] = C.QWEN_MODEL
    model["plugin_id"] = "langgenius/openai"
    model["model_provider"] = C.OPENAI_PROVIDER
    model["credential_ref"] = None
    ms = model.get("model_settings")
    if not isinstance(ms, dict):
        ms = {}
        model["model_settings"] = ms
    ms["max_tokens"] = ms.get("max_tokens") or 4096
    for k in ("stop", "top_p", "temperature", "response_format", "presence_penalty", "frequency_penalty"):
        ms.setdefault(k, None)
    return cfg


def main() -> int:
    d = Dify(C.DIFY_EMAIL, C.DIFY_PASSWORD)
    try:
        d.login()
        app_id, agent_id, snap_id, draft_id = find_ids(d)
        print("app_id", app_id)
        print("agent_id", agent_id)
        print("snap_id", snap_id)
        print("draft_id", draft_id)

        tool_entry = resolve_tool_meta(d)
        print("tool_entry", json.dumps(tool_entry, ensure_ascii=False))

        snap = AC.load_cfg(d, "agent_config_snapshots", snap_id)
        draft = AC.load_cfg(d, "agent_config_drafts", draft_id)
        AC.save_cfg(d, "agent_config_snapshots", snap_id, patch_cfg(snap, tool_entry), "hw1s")
        AC.save_cfg(d, "agent_config_drafts", draft_id, patch_cfg(draft, tool_entry), "hw1d")

        # 写回 _config 提示(不自动改文件,打印给人工确认)
        print("\n请把以下常量写入 /opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/_config.py(若尚未写):")
        print('HOMEWORK1_APP_ID = "%s"' % app_id)
        print('HOMEWORK1_AGENT_ID = "%s"' % agent_id)
        print('HOMEWORK1_SNAP_ID = "%s"' % snap_id)
        print('HOMEWORK1_DRAFT_ID = "%s"' % draft_id)
        print("\n[WARN] 改完不要点「构建/发布」;强刷后新开对话验收。")
        return 0
    finally:
        d.close()


if __name__ == "__main__":
    sys.exit(main())

UI 等价(部分):

  1. 进入 运维巡检 Agent → 工具 → 添加 getServerStatus
  2. 配置 → 写 System Prompt(与脚本中 SYSTEM_PROMPT 一致)
  3. 选模型 qwen3.6-27b
  4. 不要点发布;强刷页面

3.5 步骤 5 · 对话验收

理论

验收必须 新开对话(conversation_id="")。失败会话 status=error 上继续问会干扰结果。

操作

完整程序:/opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_05_对话验收.py

运行/引用:/usr/bin/python3 /opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_05_对话验收.py

python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""作业 1 · 步骤 5:对话验收 —— 问 gpu-server-01 状态,应触发工具并返回指标。"""
from __future__ import annotations

import sys

import _bootstrap  # noqa: F401
import _config as C
from dify_api import Dify, pp


def resolve_agent_id(d: Dify) -> str:
    aid = getattr(C, "HOMEWORK1_AGENT_ID", "") or ""
    if aid:
        return aid
    sql = (
        "SELECT a.id FROM apps ap JOIN agents a ON a.app_id=ap.id "
        "WHERE ap.name='%s' ORDER BY ap.created_at DESC LIMIT 1;"
        % C.HOMEWORK1_APP_NAME.replace("'", "''")
    )
    out, err = d.sh(
        'docker exec %s psql -U postgres -d dify -t -A -c "%s"'
        % (C.PG_CONTAINER, sql)
    )
    aid = (out or "").strip()
    if not aid:
        raise RuntimeError("找不到 Agent: %s %s" % (out, err))
    return aid


def main() -> int:
    d = Dify(C.DIFY_EMAIL, C.DIFY_PASSWORD)
    try:
        d.login()
        agent_id = resolve_agent_id(d)
        print("agent_id", agent_id)
        r = d.post(
            "/agent/%s/chat-messages" % agent_id,
            body={
                "inputs": {},
                "query": C.HOMEWORK1_PROBE_QUERY,
                "response_mode": "streaming",
                "files": [],
                "conversation_id": "",
            },
            extra="-m 420 --max-time 420",
            t=450,
        )
        pp("chat", r, limit=4000)
        text = r.get("text") or ""
        ok_markers = ("45%", "62%", "78%", "18.2", "gpu-server-01", "getServerStatus")
        hits = [m for m in ok_markers if m in text]
        print("\n匹配到的验收关键词:", hits)
        if r.get("code") not in ("200",) or len(hits) < 2:
            print("\n[FAIL] 未看到工具返回的模拟指标,请检查工具挂载与 SSRF 出网。")
            return 1
        print("\n[PASS] Agent 已调用工具并返回状态信息。")
        return 0
    finally:
        d.close()


if __name__ == "__main__":
    sys.exit(main())

UI 手工验收

  1. 打开 http://192.168.110.207 → 运维巡检 Agent
  2. 新开对话
  3. 发送:帮我看看 gpu-server-01 的状态
  4. 期望回答:
text
服务器:gpu-server-01
CPU:45%
内存:62%
GPU 利用率:78%
GPU 显存:18.2/24 GB

第四部分 CPU 与 GPU 在本作业中的分工(当前 + 未来)

本作业涉及两类算力:207 CPU 跑模拟 API;116 GPU 跑 Qwen 工具调用推理。

即使你现在只有 CPU 机做 Dify,LLM 仍在 GPU 机;将来有 GPU 资源时,下面「未来路径」可直接照做。

4.1 当前架构(实录)

组件机器算力端口作用
FastAPI 模拟 API207CPU8777返回假监控数据
Dify 全家桶207CPU + 内存80/443Agent 编排、调工具
vLLM Qwen3.6-27B116GPU H208000决定是否调工具、格式化回答
Langfuse(可选)207CPU3000Trace 观测

数据流算力分布:

text
用户问句 ──► 207 Dify ──► 116 GPU(2 次 LLM)──► 207 CPU(1 次 HTTP GET)──► 116 GPU(汇总回答)

瓶颈通常在 116 vLLM 排队(max-num-seqs=8),不在 FastAPI(毫秒级)。

4.2 CPU 路径 · 模拟 API 与 Dify(当前推荐)

项建议
部署位置207 本机 /opt/server_status_api
端口8777(8765 已被「办公 IP 同步控制台」占用)
进程uvicorn + nohup,PID 写 /tmp/server_status_api.pid
依赖pip3 install fastapi uvicorn(207 默认无 fastapi)
资源单进程 <100MB 内存;无 GPU 需求
扩展真实监控对接时,仍放 207 或内网 API 网关;保持 Dify 能 curl 到

CPU 路径注意:

4.3 GPU 路径 · LLM 工具调用(116 vLLM)

工具调用必须在支持 function calling 的 LLM 上跑。本环境 Qwen3.6-27B 在 116 GPU。

参数当前值说明
--tool-call-parserqwen3_xml用 qwen3 会 KeyError
--enable-auto-tool-choice开允许模型自选工具
--reasoning-parserqwen3thinking 计入 max_tokens
--max-num-seqs8超过则排队,工具 Agent 也慢
--gpu-memory-utilization0.75与 mxh 业务共享 H20

验收工具调用时的 GPU 检查:

bash
# 116 上
nvidia-smi
curl -s -H "Authorization: Bearer 123456" http://127.0.0.1:8000/v1/models

# 207 上测跨机
curl -s -H "Authorization: Bearer 123456" http://116.62.10.183:8000/v1/models

带 tools 的 smoke(专题五):

bash
curl -s -X POST http://116.62.10.183:8000/v1/chat/completions \
  -H "Authorization: Bearer 123456" -H "Content-Type: application/json" \
  -d '{"model":"qwen3.6-27b","messages":[{"role":"user","content":"hi"}],
       "tools":[{"type":"function","function":{"name":"getServerStatus",
       "parameters":{"type":"object","properties":{"server_name":{"type":"string"}},"required":["server_name"]}}}],
       "tool_choice":"auto"}'

4.4 未来有 GPU 资源时 · 可选演进

场景CPU 现状有 GPU 后可选
模拟 API207 CPU 足够可不变;或迁 K8s / 容器
LLM 工具调用116 GPU 已满足加 max-num-seqs 或第二副本
真实 GPU 监控 API返回假 gpu_util对接 Prometheus / DCGM,仍走 OpenAPI 工具
Embedding/Rerank本作业不涉及若同一 Agent 又要知识库又要工具,Rerank 建议 GPU(专题十三)

未来在同一 Agent 上叠加知识库 + HTTP 工具:

  1. 快照 knowledge.sets 绑库 + tools.dify_tools 挂 API 工具(可同时存在)。
  2. Prompt 写清优先级:「查制度用知识库,查机器状态用 getServerStatus」。
  3. max_tokens 建议 8192–16384(比纯知识库小,因 Prompt 更短)。
  4. GPU 并发按 两次 LLM + 可能一次检索 规划,见专题十四容量表。

4.5 本作业资源对照小结

问题CPU 相关GPU 相关
工具不调Prompt / 快照未挂工具vLLM parser 错 / 模型未配
工具调了超时8777 不通 / SSRF通常无关 GPU
回答很慢FastAPI 不是瓶颈vLLM 排队或 thinking 长
400 Internal快照 model 空少见

第五部分 完整代码程序

以下均为完整可运行程序,不是片段。索引见步骤 3.1~3.5 嵌入块。

文件用途
/opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_server_status_api.pyFastAPI 服务
scripts_bash_cursor/作业1_带工具调用的Agent_服务器状态/作业1_启动_server_status.sh207 启动脚本
patches_cursor/作业1_带工具调用的Agent_服务器状态/作业1_server_status_openapi.yamlOpenAPI Schema
/opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_01_部署模拟服务.py部署 + 探针
/opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_02_注册OpenAPI工具.py注册 Dify 工具
/opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_03_创建Agent空壳.pyPOST http://127.0.0.1/console/api/agent
/opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_04_查ID并挂工具.py快照挂工具
/opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_05_对话验收.py自动验收
/opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_06_修复配置页InternalServerError.py配置页红条:API 工具 UUID 被当成 builtin

环境常量(HOMEWORK1_*、SERVER_STATUS_*)在 /opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/_config.py。


第六部分 本次实战问题与解决方案

以下均为 207 环境真实踩坑,按发生顺序 + 速查表。

6.1 问题速查表

#现象根因解决方案
1POST http://127.0.0.1/console/api/apps + "mode":"agent" 返回 4001.16 创建口改为 /agent用 作业1_03,完整 curl 见第三部分「创建空壳」
2invalid input syntax for type uuid: "server_status"tools.dify_tools[].provider_id 写了名称查 SELECT id FROM tool_api_providers WHERE name='server_status',写 UUID
3对话 completion_request_error / Internal 500同上重跑 作业1_04
4AttributeError: 'NoneType' model新空壳快照 model 为 null作业1_04 写入 qwen3.6-27b + max_tokens
58777 起不来 / 返回 HTML8765 已被其他 Python 服务占用本作业改用 8777;OpenAPI servers.url 同步改
6207 无 fastapi宿主机未装pip3 install fastapi uvicorn(启动脚本已含)
7psql 按中文名查 Agent 失败Windows → SSH 编码乱码用 GET http://127.0.0.1/console/api/agent?page=1&limit=50,勿拼中文进 psql
8工具注册后列表空未登录 / 权限确认 dify_api.py login 200
9模型不调工具system_prompt 太弱或未挂工具检查快照 tools.dify_tools 与 Prompt
10工具调了但 observation 空8777 从 Dify 容器不可达确认 ssrf_proxy、防火墙、servers.url 用宿主机 IP
11改完 UI 又失效点了「发布」不要发布;重跑步骤 4,新开对话
12vLLM 无 tool_callsparser 配置错--tool-call-parser qwen3_xml
13打开 Agent 配置页红条 Internal Server Error,对话验收仍通过配置页把 OpenAPI 工具 UUID 当成 builtin 插件 去拉凭据跑 作业1_06_修复配置页InternalServerError.py,硬刷新配置页

6.2 问题 1 · POST /apps 被拒(详细)

现象:

json
{"code":"invalid_param","message":"... mode ... Input should be 'chat', 'agent-chat' ..."}

根因:Dify 1.16 新 Agent 走 Roster:POST http://127.0.0.1/console/api/agent(见 roster.py AgentAppListApi.post)。

解决:作业1_03_创建Agent空壳.py 已改为 POST http://127.0.0.1/console/api/agent。不要用专题十五旧脚本里的 POST http://127.0.0.1/console/api/apps + "mode":"agent" 做本作业。


6.3 问题 2 · provider_id UUID(详细)

现象(api 日志):

text
invalid input syntax for type uuid: "server_status"
WHERE tool_api_providers.id = 'server_status'::UUID

根因:Agent 快照里 provider_id 误填了 OpenAPI provider 名称。运行时 Dify 按 UUID 查 tool_api_providers 表。

解决:

sql
SELECT id, name FROM tool_api_providers WHERE name='server_status';
-- 021a4445-098d-4821-81fe-ff0ebc660ecc

写入快照:

json
"provider_id": "021a4445-098d-4821-81fe-ff0ebc660ecc"

作业1_04 已自动查 UUID 并写入。


6.4 问题 3 · 空壳 model 为 null(详细)

现象:挂工具后对话仍 400 Internal Server Error。

根因:POST /agent 创建的空壳 config_snapshot.model 可能为 null,工具层在拉 LLM 前即失败。

解决:patch_cfg() 强制写入:

python
model["model"] = "qwen3.6-27b"
model["model_provider"] = "langgenius/openai/openai"
model["model_settings"]["max_tokens"] = 4096

6.5 问题 4 · 端口 8765 冲突(详细)

现象:/server/status 返回 404 HTML,非 JSON。

根因:8765 上跑的是「办公 IP 同步控制台」,不是本作业 API。

解决:统一改用 8777;_config.py 的 SERVER_STATUS_PORT、openapi.yaml 的 servers.url、防火墙规则三者一致。


6.6 验收通过记录(便于对照)

命令:/usr/bin/python3 /opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_05_对话验收.py

结果摘要:


6.7 问题 13 · 打开配置页 Internal Server Error(对话仍正常)

现象:问「gpu-server-01 的状态」能调工具并答对;一进 配置 页,右上角弹出一条或多条红色 Internal Server Error。

不是:CPU/GPU 资源不足。vLLM 正常、8777 正常、messages.status=normal。

根因:Dify 1.16 新 Agent 配置页会请求:

bash
curl -s -b /tmp/dify_cj.txt -H "X-CSRF-Token: $CSRF" \
  'http://127.0.0.1/console/api/workspaces/current/tool-provider/builtin/021a4445-098d-4821-81fe-ff0ebc660ecc/credential/info'

本作业工具是 OpenAPI(api),provider_id 是 UUID 021a4445-098d-4821-81fe-ff0ebc660ecc。配置页把它当成 builtin 插件 ID,plugin_daemon 返回 PluginNotFoundError,接口 500,前端弹出红条。

解决(已在 207 打上):

bash
/usr/bin/python3 /opt/workbuddy/scripts_cursor/作业1_带工具调用的Agent_服务器状态/作业1_06_修复配置页InternalServerError.py

该脚本从镜像恢复被误改的文件后,只给 get_builtin_tool_provider_credential_info 加兜底:插件不存在时返回空凭据 JSON(HTTP 200)。

你现在要做:浏览器 强制刷新(Ctrl+F5)再打开「运维巡检 Agent」配置页,红条应消失。对话能力不变。

完整专题(原因、步骤、修改前/后完整代码):Dify_专题十六_新Agent配置页InternalServerError_cursor.md / .html

注意:重建 docker-api-1 容器后补丁会丢,需再跑一遍 作业1_06。


第七部分 对本系统的专业建议

7.1 架构

  1. 模拟 API 与 Dify 同机(207):内网延迟低,ssrf 出网简单;生产换真实监控 API 时只改 OpenAPI URL,Agent 逻辑不变。
  2. 工具 Provider 工作区级复用:server_status 注册一次,多个 Agent 可挂同一工具;新 Agent 仍要注意 UUID 写入快照。
  3. 与知识库 Agent 分离:本作业「运维巡检 Agent」与「行政问答 Agent」分开,避免 Prompt 互相干扰;成熟后可合并为一个 Agent(见 §4.4)。

7.2 运维

  1. 8777 进程纳入巡检:curl /health;重启 207 后执行 bash /opt/server_status_api/start.sh。
  2. Xinference / vLLM 重启不影响本作业 HTTP 工具,但会影响若同一 Agent 挂了知识库的情况。
  3. 验收永远新开对话;自动化用 conversation_id=""。
  4. 改快照后禁止点发布;与专题十一一致。

7.3 安全

  1. 模拟 API 无鉴权 → 仅内网;上生产必须加 API Key 或 mTLS,并在 Dify 工具 credentials 配置。
  2. OpenAPI 不要指向内网敏感接口(如 K8s API、数据库)除非有鉴权与 SSRF 白名单。
  3. _config.py 含控制台密码,勿泄露。

7.4 培训与扩展

下一步建议
作业 2再加一个 HTTP 工具(如重启服务),练多工具选择
工作流工具 + 条件分支(Dify Workflow)
真实监控FastAPI 改读 Prometheus,字段保持兼容
GPU优先保证 vLLM parser;模拟 API 仍 CPU 即可

第八部分 验收清单

8.1 部署侧

8.2 Agent 配置侧

8.3 对话侧

8.4 GPU 侧(116)


关联文档

文档何时看
专题十五新 Agent 通用创建
专题四 / 九vLLM 工具 parser
专题十一勿点发布
专题十四vLLM 排队变慢
内部智能体配置文档专题汇总全局总纲

*作业 1 已于 207 环境跑通验收;变更端口或 UUID 后请同步 _config.py 与 OpenAPI yaml。*