版本: v1.0 | 日期: 2026-03-26
目標: 讓使用者用自然語言描述需求,系統自動產出客製報表
使用者不需要寫程式或編輯模板。透過 Chat UI 描述需求,AI Agent 自動:
使用者:「我想要一份報表,只看 Windows 伺服器的磁碟使用率,
超過 70% 的用紅色標示,每週一寄給 IT 主管」
Agent:我來幫你建立這份報表。
資料來源:LibreNMS API → /devices (os=windows) + /storage
篩選條件:os = windows
欄位:hostname, IP, 各磁碟使用率
標示規則:> 85% 紅色, > 70% 黃色, 其餘綠色
排程:每週一 09:00
寄送:IT 主管 Email
[預覽報表] [確認並儲存] [修改條件]
使用者想要的資料已經在現有 Tools 能取得的範圍內,只是組合方式不同。
範例:
Agent 做法:
// Agent 收到:「我要一份只看交換器的 Port 流量報表」
// Agent 自動執行:
// 1. 查所有設備,篩選交換器
const devices = await list_devices({ status: "active" });
const switches = devices.filter(d => d.os.match(/comware|arubaos|jetstream/));
// 2. 查每台交換器的 Port 流量
const portData = await Promise.all(
switches.map(sw => get_device_ports({ hostname: sw.hostname }))
);
// 3. 組裝報表資料,傳入模板
const report = await generate_report({
title: "交換器 Port 流量報表",
template: "custom", // 通用模板
sections: [...]
});
使用者要的資料存在,但想要不同的呈現方式或跨資料源的關聯分析。
範例:
Agent 做法:
// Agent 收到:「我要每台設備的健康分數」
// Agent 自主設計評分邏輯:
function calculateHealthScore(device) {
let score = 100;
if (device.cpu > 90) score -= 30;
else if (device.cpu > 70) score -= 10;
if (device.memory > 90) score -= 30;
else if (device.memory > 70) score -= 10;
if (device.disk > 85) score -= 20;
else if (device.disk > 70) score -= 5;
if (device.status === 0) score = 0;
return score;
}
// Agent 動態產生 HTML(不需要預設模板)
const html = `
<table>
<tr><th>設備</th><th>健康分數</th><th>CPU</th><th>Mem</th><th>Disk</th></tr>
${devices.map(d => `
<tr>
<td>${d.hostname}</td>
<td style="color:${d.score > 80 ? '#10b981' : d.score > 50 ? '#f59e0b' : '#ef4444'}">
<strong>${d.score}</strong>/100
</td>
...
</tr>
`).join('')}
</table>`;
使用者要的資料超出現有 LibreNMS/Graylog 的範圍。
範例:
處理流程:
使用者:「我要報表裡加入 vCenter 的 VM 資訊」
│
Agent: 分析需求 → 發現沒有 vCenter Tool
│
▼
Agent: 「目前系統尚未整合 VMware vCenter。
要加入這個資料源,需要:
1. vCenter REST API 連線資訊(URL + 帳號密碼)
2. 開發 vCenter Tool(約 2 天工期)
我先幫你建立一份需求單:
┌──────────────────────────────┐
│ 需求:vCenter 整合 │
│ 資料:VM 清單、CPU/Mem 使用率 │
│ API:vSphere REST API │
│ 優先級:[請選擇] │
└──────────────────────────────┘
同時,我可以先用 LibreNMS 的 ESXi 資料
產出一份近似的報表,要嗎?」
新 Tool 開發流程:
1. 定義 Tool Schema(Zod)
↓
2. 實作 API Client(連線 + 認證)
↓
3. 建立 Skill YAML(加入 Agent)
↓
4. 測試 Tool Calling
↓
5. 重啟 Agent → Supervisor 自動納入新 Skill
# skills/vcenter/skill.yaml — 新增的 Skill
name: vcenter_expert
description: "VMware vCenter 虛擬化管理、VM 清單、資源使用率、快照"
model:
default: ollama/qwen3.5:35b-a3b
tools:
- list_vms
- get_vm_metrics
- get_datastore_usage
- get_host_status
prompt: |
你是 VMware vCenter 專家...
使用者要的不只是新資料,而是一個全新的功能領域。
範例:
處理流程: Agent 引導使用者定義需求,產出 PRD → 開發團隊實作。
使用者建立的自訂報表需要可以儲存和重複使用。
# saved-reports/windows-disk-weekly.yaml
name: Windows 伺服器磁碟週報
description: 所有 Windows 伺服器的磁碟使用率,超過 70% 標示警告
created_by: kyle
created_at: 2026-03-26
# 資料查詢
queries:
- tool: list_devices
params: { status: active }
filter: "os CONTAINS 'windows'"
- tool: get_device_health
params: { hostname: "$each.hostname", metric: storage }
# 報表設定
report:
template: device-health-check # 基於現有模板(或 "custom" 用動態 HTML)
title: "Windows 伺服器磁碟使用率週報"
columns:
- { field: hostname, label: 設備名稱 }
- { field: ip, label: IP }
- { field: storage.descr, label: 磁碟 }
- { field: storage.percent, label: 使用率, format: percent, thresholds: { warn: 70, critical: 85 } }
sort: storage.percent DESC
highlight:
- condition: "storage.percent > 85"
style: red
- condition: "storage.percent > 70"
style: yellow
# 排程
schedule:
cron: "0 9 * * 1" # 每週一 09:00
notify:
- type: email
to: it-manager@auras.com.tw
subject: "[Arova] Windows 磁碟週報 — {{date}}"
// Chat UI 中的報表管理
使用者:「列出我儲存的報表」
Agent:
1. Windows 伺服器磁碟週報 — 每週一 09:00
2. 交換器 Port 流量日報 — 每天 17:00
3. AD 特權帳戶月報 — 每月 1 日
使用者:「修改第 1 個,閾值改成 60%」
Agent:已更新。磁碟使用率 > 60% 將標示黃色,> 85% 標示紅色。
使用者:「現在跑一次看看」
Agent:[執行查詢 → 產出報表]
📎 Windows_磁碟週報_2026-03-26.pdf
摘要:12 台 Windows 伺服器,3 台超過 60% 閾值:
- th-ex001 D: 78%
- mes-db-2 E: 72%
- th-qmdb C: 65%
使用者:「我想建一份新報表」
│
Agent: 「好的,請告訴我:
1. 這份報表要看什麼資料?
2. 要定時產出還是手動觸發?
3. 要寄給誰?」
│
使用者:「我要看所有設備的 CPU 趨勢,過去 7 天每天的平均值,
超過 80% 的用紅色。每週五下午寄給我。」
│
Agent: 分析需求:
├── 資料:LibreNMS → list_devices + get_device_health (processor)
├── 時間:7 天,按日聚合
├── 閾值:80% = 紅色
├── 排程:cron "0 17 * * 5"
└── 通知:Email → 使用者
│
Agent: 「我設計了這份報表:
📊 設備 CPU 7 日趨勢報表
─────────────────────
| 設備 | 週一 | 週二 | ... | 週五 | 平均 |
|----------|------|------|-----|------|------|
| server-1 | 45% | 52% | ... | 61% | 53% |
| server-2 | 82% | 85% | | 79% | 82% | ← 紅色
要我先跑一次預覽嗎?」
│
使用者:「好,跑一次看看」
│
Agent: [執行查詢 → 動態產生 HTML → PDF]
📎 CPU_7日趨勢_2026-03-26.pdf
│
使用者:「不錯,存起來,以後每週五自動跑」
│
Agent: [儲存 YAML → 加入 BullMQ 排程]
「已儲存為『設備 CPU 7日趨勢報表』,
每週五 17:00 自動產出並寄送。」
新增一個專門處理自訂報表的 Tool:
// src/tools/report-builder.ts
import { tool } from "@langchain/core/tools";
import { z } from "zod";
export const create_custom_report = tool(
async ({ title, queries, columns, thresholds, sort }) => {
// 1. 執行每個 query
const data = await executeQueries(queries);
// 2. 套用篩選、排序
const processed = applyFiltersAndSort(data, columns, sort);
// 3. 套用閾值標色
const styled = applyThresholds(processed, thresholds);
// 4. 動態產生 HTML(不依賴固定模板)
const html = renderDynamicTable(title, styled, columns);
// 5. 轉 PDF
const pdfPath = await htmlToPdf(html);
return JSON.stringify({ htmlPath, pdfPath, summary: generateSummary(styled) });
},
{
name: "create_custom_report",
description: "根據使用者描述的條件,動態查詢資料並產出客製報表",
schema: z.object({
title: z.string(),
queries: z.array(z.object({
tool: z.string().describe("要呼叫的 Tool 名稱"),
params: z.record(z.any()).describe("Tool 參數"),
filter: z.string().optional().describe("結果篩選條件"),
})),
columns: z.array(z.object({
field: z.string(),
label: z.string(),
format: z.enum(["text", "percent", "bytes", "number", "date"]).default("text"),
})),
thresholds: z.array(z.object({
field: z.string(),
warn: z.number(),
critical: z.number(),
})).optional(),
sort: z.string().optional().describe("排序欄位和方向,如 'cpu DESC'"),
}),
}
);
export const save_report_config = tool(
async ({ name, config, schedule }) => {
// 儲存 YAML 到 saved-reports/
const yamlContent = yaml.stringify({ name, ...config, schedule });
writeFileSync(`./saved-reports/${slugify(name)}.yaml`, yamlContent);
// 如果有排程,加入 BullMQ
if (schedule) {
await reportQueue.add(slugify(name), config, {
repeat: { pattern: schedule.cron }
});
}
return JSON.stringify({ saved: true, path: `saved-reports/${slugify(name)}.yaml` });
},
{
name: "save_report_config",
description: "儲存自訂報表設定,可選加入定時排程",
schema: z.object({
name: z.string(),
config: z.any(),
schedule: z.object({
cron: z.string(),
notify: z.array(z.object({
type: z.enum(["email", "line"]),
to: z.string(),
})),
}).optional(),
}),
}
);
export const list_saved_reports = tool(
async () => {
const reports = readdirSync("./saved-reports")
.filter(f => f.endsWith(".yaml"))
.map(f => yaml.parse(readFileSync(`./saved-reports/${f}`, "utf-8")));
return JSON.stringify(reports);
},
{
name: "list_saved_reports",
description: "列出所有已儲存的自訂報表",
schema: z.object({}),
}
);
# skills/report/skill.yaml — 升級版
name: report_generator
description: "產出報表:預設模板或自訂報表。管理已儲存的報表設定和排程。"
tools:
- generate_report # 用預設 Handlebars 模板
- create_custom_report # 動態自訂報表(無需模板)
- save_report_config # 儲存報表設定
- list_saved_reports # 列出已儲存報表
- render_pdf # HTML → PDF
- send_email # Email 寄送
prompt: |
你是報表產生專家。你有兩種方式產出報表:
1. 預設模板(daily-security-brief, device-health-check, weekly-analysis,
monthly-security-report, device-diagnostic)— 用 generate_report
2. 自訂報表 — 用 create_custom_report 動態產生
當使用者描述了一個不符合預設模板的需求時,使用此方式。
你需要:理解需求 → 設計查詢 → 定義欄位和閾值 → 產出報表。
使用者說「存起來」或「以後自動跑」時,用 save_report_config 儲存設定。
使用者問「我有哪些報表」時,用 list_saved_reports 列出。
重要:產出報表前,先跟使用者確認欄位和條件。
產出後,提供摘要(幾筆資料、幾筆異常)讓使用者快速判斷。
// Supervisor 的 system prompt 加入此段
const SUPERVISOR_PROMPT = `
...
## 處理使用者要求新資料源
如果使用者要求的資料超出現有 Tools 的能力,按以下步驟處理:
1. 明確告知使用者「目前系統尚未整合 [X]」
2. 說明需要什麼才能整合(API 連線資訊、開發工期)
3. 提出替代方案(「我可以先用 LibreNMS 的 [Y] 資料產出近似報表」)
4. 如果使用者確認要新增,幫他記錄需求:
- 資料源名稱
- 需要的欄位
- 連線方式(REST API / SSH / SNMP)
- 優先級
5. 儲存為 feature-requests/<name>.yaml 供開發團隊參考
`;
使用者建自訂報表時,可以組合的資料維度:
| 維度 | Tool | 可用欄位 |
|---|---|---|
| 設備清單 | list_devices |
hostname, ip, os, version, hardware, status, uptime, location, serial |
| CPU | get_device_health |
processor_descr, processor_usage |
| 記憶體 | get_device_health |
mempool_descr, mempool_perc, mempool_used, mempool_total |
| 磁碟 | get_device_health |
storage_descr, storage_perc, storage_used, storage_size, storage_free |
| Port 流量 | get_device_ports |
ifName, ifAlias, ifInOctets_rate, ifOutOctets_rate, ifOperStatus, ifSpeed |
| 告警 | get_active_alerts |
severity, rule, hostname, timestamp, state |
| 網路拓撲 | get_links |
local_device, local_port, remote_hostname, remote_port, protocol |
| Sensor | (可擴充) | temperature, voltage, fanspeed, humidity |
| 維度 | Tool | 可用欄位 |
|---|---|---|
| 防火牆流量 | get_firewall_stats |
action(allow/deny), src_ip, dst_ip, dst_port, application, bytes |
| AD 登入 | get_ad_login_stats |
EventID, TargetUserName, IpAddress, success/failure |
| Log 搜尋 | search_logs |
任意 Graylog 欄位(Lucene 語法) |
| 告警歷史 | (可擴充) | event_definition, priority, timestamp |
| 使用者需求 | 資料組合 |
|---|---|
| 「各設備的健康評分排行」 | list_devices + get_device_health → 計算綜合分數 |
| 「被阻擋最多的內部 IP」 | get_firewall_stats (deny, 篩選 src_ip=10.20.*) |
| 「磁碟快滿的設備 + 最近的相關 log」 | get_device_health (storage>80%) + search_logs |
| 「交換器 Port 使用率 + LLDP 連接對象」 | get_device_ports + get_links |
| 「非工作時間登入成功的帳戶清單」 | search_logs (EventID:4624 AND hour:[22 TO 6]) |
| 「本週新出現的被阻擋 IP」 | get_firewall_stats (本週 Top IP) vs (上週 Top IP) → diff |
首次使用
│
├─ 使用者:「我要看所有設備的磁碟」
│ Agent:[用預設 device-health-check 模板]
│ 📎 設備健康巡檢.pdf
│
├─ 使用者:「只看 Windows 的,超過 70% 標紅」
│ Agent:[用 create_custom_report 動態產生]
│ 📎 Windows_磁碟報表.pdf
│
├─ 使用者:「不錯,每週一自動跑」
│ Agent:[save_report_config + BullMQ 排程]
│ ✅ 已儲存,每週一 09:00 自動寄送
│
├─ 使用者:「加入 vCenter 的 VM 資訊」
│ Agent:「目前沒有 vCenter 整合。
│ 我先用 ESXi 的資料替代,
│ 同時記錄需求給開發團隊。」
│
進階使用
│
├─ 使用者:「我的報表有哪些?」
│ Agent:[list_saved_reports] → 列出清單
│
├─ 使用者:「把第 2 個的閾值改成 60%」
│ Agent:[更新 YAML] → 「已更新」
│
└─ 使用者:「幫我設計一份給廠長看的月報」
Agent:「廠長通常關心:
1. 整體健康分數(一個數字)
2. 本月重大事件(如有)
3. 與上月比較趨勢
4. 需要投資的項目
要用這個架構嗎?」
使用者:「好」
Agent:[設計模板 → 產出 → 預覽 → 儲存]