前言

當你用試算表管理 WBS(工作分解結構圖)時,常會遇到這些情況:

  • 儲存格合併和顏色分類越來越多,最後只有原本建立的人能碰
  • 無法追蹤差異,搞不清楚「從上週開始多了什麼」
  • 想交給 AI 處理時,得先從匯出 CSV 開始
  • 可以算出功能別工時,卻無法立刻算出工程別工時(或反過來)

這篇文章整理的是:把 WBS 寫成 每行一個任務的扁平 Markdown 表格,並同時從功能軸與工程軸彙總工時的方法。內容包含以 AI 編輯為前提的欄位設計,以及為了不盲信 AI 的彙總結果而準備的驗算腳本。

※本文是個人的運用備忘錄。內容以 2026-08-05 當時確認的資訊為準。Markdown 表格的行為是根據 GitHub Flavored Markdown(GFM)規範撰寫,文中的行為也已在參照實作 cmark-gfm 上實際確認。不過,包含 Qiita 在內的各服務 Markdown 渲染器,行為不一定完全一致。

本文涵蓋範圍

intro.png

本文只聚焦以下三點:

  1. AI 不容易弄壞的 WBS 表格寫法(欄位設計規則)
  2. 功能軸、工程軸、交叉彙總的產出方式
  3. 驗算 AI 彙總結果的機制(腳本與 Git 運用)

另一方面,以下內容不會涵蓋:

  • 特定專案管理工具(Jira、Backlog、MS Project 等)的使用方式
  • 估算技法本身(三點估算法、Planning Poker 等)
  • 依賴關係分析與關鍵路徑計算
  • 真實案件的實際資料

結論:做成每行一個任務的扁平表格,彙總另外產生

fig_02_conclusion.png

先講結論。適合用 AI 編輯的 WBS,具備以下四點:

  1. 做成每行一個任務的扁平表格 — 階層不用縮排或儲存格合併表現,而是用 功能 工程 這些欄位表示
  2. 軸放在欄位中 — 不要分開做功能軸表與工程軸表。從同一張表生成兩種檢視
  3. 不要把彙總列混進表內 — 如果合計列混在裡面,AI 一旦新增列,合計與明細就會錯位
  4. 彙總每次都用腳本重新產生 — 可以讓 AI 幫忙加總,但不要直接相信

簡單說,就是把 WBS 表格當成 「彙總的輸入資料」,而不是 「給人類閱讀的最終成品」

為什麼不用試算表,而用 Markdown

fig_03_why_markdown.png

不是試算表不好,而是當你以 AI 與 Git 為前提時,「文字」本身就變得很有價值。

1. 差異可以以行為單位閱讀

.xlsx 是由 ZIP 壓縮的 XML 集合(Office Open XML),所以在 Git 裡會被當成二進位檔。實際上,只改動 xlsx 內的一個地方後提交並看差異,會像這樣:

$ git diff --stat
 wbs.xlsx | Bin 299 -> 299 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)

$ git diff
diff --git a/wbs.xlsx b/wbs.xlsx
index 433c46f..6bf598d 100644
Binary files a/wbs.xlsx and b/wbs.xlsx differ

看不出來「是哪個任務的估算多了幾天」。如果是 Markdown 表格,只有變動的那一行會出現在 diff 裡。

-| T-007 | 承認流程 | 實作 | 承認/退回 API 的實作 | 鈴木 | 5 | 5 | doing |
+| T-007 | 承認流程 | 實作 | 承認/退回 API 的實作 | 鈴木 | 8 | 5 | doing |

2. 可以直接交給 AI

Markdown 表格是純文字,可以直接把檔案讀給 AI。像「把 T-007 的估算改成 8 人日,理由寫在備註」這種局部修改,因為可以明確定位列,所以指示也更容易。

3. 可以放到審查流程的平台上

WBS 更新可以做成 Pull Request。工時增減會以 diff 的形式留下來,因此可以用留言記錄「為什麼這個估算增加了」。

割捨之處

當然,也有失去的東西:

  • 透過公式自動彙總(Markdown 表格沒有計算公式)
  • 排序與篩選的 UI
  • 同時編輯與儲存格級留言

其中 自動彙總可以由後面會提到的腳本替代。如果排序與篩選真的已經成為必要功能,則應該果斷移到專用工具。

本文中的「功能軸」「工程軸」

fig_04_wbs_basics.png

本文在彙總 WBS 時,將「軸」定義為以下意思:

稱呼意義本文中的欄位功能軸要做什麼(功能/成果物單位)功能工程軸處於哪個階段的工作(需求定義、設計、實作…)工程這兩個軸不分開管理成兩張表,而是作為欄位放進同一份任務表。如此一來,就能從同一份主表產生功能別與工程別兩種彙總。

另外,腳本能確認的是 已登錄任務的彙總是否一致。至於是否少了必要任務,則由人來審查確認。

以一張表格同時放進功能軸與工程軸

fig_05_two_axes.png

常見的錯誤是,把功能軸 WBS 與工程軸 WBS 分別放在不同工作表。結果只更新其中一邊,總和就不一致了。

因此,做法改成這樣:

  • 實體(主表)只有 每行一個任務的扁平表格 這一份
  • 功能軸、工程軸、交叉彙總都視為從主表 生成的檢視

如果用試算表來類比,就像樞紐分析表。只要主表只有一份,更新點也就只有一處。

wbs.md(主表:每行一個任務)
   │
   ├─▶ 功能軸彙總(各功能的估算/實績)
   ├─▶ 工程軸彙總(各工程的估算/實績)
   └─▶ 功能 × 工程的交叉彙總

AI 不容易弄壞的欄位設計與範本

fig_06_columns.png

以讓 AI 編輯為前提時,欄位設計有一些有用的規則:

  1. ID 固定且不可變 — 使用 T-001 這種連號,即使重新排序也不要變更值。這樣就能對 AI 下達像「把 T-007 的估算改成 8 人日」這樣的指令
  2. 軸要做成欄位 — 不要用縮排或儲存格合併來表示階層。只要 功能 工程 的值一致,就能彙總
  3. 數值欄位的單位寫在標題上,儲存格只放數字 — 標題寫成 估算(人日),儲存格放 3。如果寫成 3人日,彙總時就無法直接當數字處理
  4. 不要把合計列混進表內 — 合計要另外生成
  5. 空白也放成 - — 這樣從外觀上就能察覺欄位是否錯位
  6. 不要在儲存格內換行 — 依照 GFM 規範,表格儲存格不能放區塊元素。長說明請移到別的檔案,或讓 備註 保持簡短
  7. 儲存格內要寫 | 時,請用 \| 跳脫 — 直接寫會多出一欄
  8. 狀態用固定詞彙 — 限定成 todo / doing / done 之類,AI 與人類都不會表記混亂

依照這些規則撰寫的範本如下(請存成 wbs.md)。

| ID | 功能 | 工程 | 任務 | 負責人 | 估算(人日) | 實績(人日) | 狀態 |
| --- | --- | --- | --- | --- | ---: | ---: | --- |
| T-001 | 申請表單 | 需求定義 | 梳理輸入項目與驗證條件 | 佐藤 | 2 | 2 | done |
| T-002 | 申請表單 | 設計 | 建立畫面流程圖與 API 請求規格 | 佐藤 | 3 | 3 | done |
| T-003 | 申請表單 | 實作 | 實作表單畫面 | 田中 | 5 | 6 | doing |
| T-004 | 申請表單 | 測試 | 輸入值的邊界值測試 | 田中 | 2 | 0 | todo |
| T-005 | 核准流程 | 需求定義 | 整理核准路線的條件 | 佐藤 | 3 | 3 | done |
| T-006 | 核准流程 | 設計 | 狀態轉移與資料表設計 | 鈴木 | 4 | 4 | done |
| T-007 | 核准流程 | 實作 | 實作核准/退回 API | 鈴木 | 8 | 5 | doing |
| T-008 | 核准流程 | 測試 | 核准路線的全面測試 | 鈴木 | 4 | 0 | todo |
| T-009 | 清單/搜尋 | 設計 | 整理搜尋條件與排序規格 | 田中 | 2 | 2 | done |
| T-010 | 清單/搜尋 | 實作 | 實作清單畫面與分頁 | 田中 | 5 | 0 | todo |
| T-011 | 清單/搜尋 | 測試 | 大量資料的顯示確認 | 田中 | 2 | 0 | todo |
| T-012 | 通知 | 實作 | 寄送核准請求信件的處理 | 鈴木 | 3 | 0 | todo |
| T-013 | 共通基礎 | 設計 | 設計認證/權限模型 | 佐藤 | 3 | 3 | done |
| T-014 | 共通基礎 | 實作 | CI 設定與自動執行測試 | 田中 | 2 | 2 | done |
| T-015 | 共通基礎 | 上線 | 撰寫正式環境部署手冊 | 佐藤 | 2 | 0 | todo |

把數值欄位分隔列寫成 ---:,就會靠右對齊,數字排列更整齊、也更容易閱讀。

以功能軸彙總工時

fig_07_rollup_feature.png

功能 欄位分組,彙總估算與實績。執行後述腳本時,會輸出下表:

| 功能 | 估算(人日) | 實績(人日) | 任務數 |
| --- | ---: | ---: | ---: |
| 申請表單 | 12 | 11 | 4 |
| 核准流程 | 19 | 12 | 4 |
| 清單/搜尋 | 9 | 2 | 3 |
| 通知 | 3 | 0 | 1 |
| 共通基礎 | 7 | 5 | 3 |
| **合計** | **50** | **30** | 15 |

從功能軸來看,可以討論出這些事情:

  • 核准流程有 19 人日,約佔整體四成。這裡若延誤,整體也會延誤
  • 通知只有 3 人日,需求定義、設計、測試的任務都沒立出來,但真的不需要嗎
  • 如果要縮減範圍,能不能整個砍掉某個功能

「砍掉一個功能」這種判斷,若沒有以功能軸彙整工時,是很難做的。

以工程軸彙總工時

fig_08_rollup_phase.png

把同一張表依 工程 欄位分組後,結果如下:

| 工程 | 估算(人日) | 實績(人日) | 任務數 |
| --- | ---: | ---: | ---: |
| 需求定義 | 5 | 5 | 2 |
| 設計 | 12 | 12 | 4 |
| 實作 | 23 | 13 | 5 |
| 測試 | 8 | 0 | 3 |
| 上線 | 2 | 0 | 1 |
| **合計** | **50** | **30** | 15 |

工程軸是用來思考排程與人力配置的。

  • 實作 23 人日,測試 8 人日。這個比例是否合理,需要檢視
  • 設計已經完成的工程與接下來的工程,需要的人手不同
  • 即使需求定義與設計的實績都符合估算,只要實作延誤,剩餘工作的預估也會改變

此外,也可以做二軸交叉彙總。空白(-)表示「那個任務根本沒立出來」,因此 很有助於找缺漏

| 功能 | 需求定義 | 設計 | 實作 | 測試 | 上線 | 合計 |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| 申請表單 | 2 | 3 | 5 | 2 | - | 12 |
| 核准流程 | 3 | 4 | 8 | 4 | - | 19 |
| 清單/搜尋 | - | 2 | 5 | 2 | - | 9 |
| 通知 | - | - | 3 | - | - | 3 |
| 共通基礎 | - | 3 | 2 | - | 2 | 7 |
| **合計** | 5 | 12 | 23 | 8 | 2 | **50** |

這個例子中,通知缺少設計與測試任務,而清單/搜尋缺少需求定義。這可以拿來判斷,究竟是刻意省略,還是單純漏了。

讓 AI 彙總時的陷阱

fig_09_pitfalls.png

這裡是重點。當你請 AI「把這張表彙總」時,它很快就會回傳看起來合理的表格;但如果直接拿來用,會踩到一些坑。

1. 欄位增加了,也會安靜消失

GFM 規範中,如果某一行的儲存格數量比標題行多,超出的部分會被忽略。少的話則補空格。以 cmark-gfm 參照實作確認後如下。

輸入:

| ID | 功能 | 估算 |
| --- | --- | --- |
| T-001 | 申請表單 | 2 | 追加的備註 |
| T-002 | 核准流程 |

輸出(節錄):

<tr><td>T-001</td><td>申請表單</td><td>2</td></tr>
<tr><td>T-002</td><td>核准流程</td><td></td></tr>

追加的備註 不會報錯,而是直接消失。即使 AI 只在某一行多加一欄,在預覽裡也不容易察覺。

2. 標題與分隔行欄位數不同時,表格甚至不會成立

如果標題行與分隔行的儲存格數量不一致,GFM 不會把它認定為表格。AI 新增了一欄卻忘記修分隔行時,整個表格就會變成普通段落。

| ID | 功能 | 估算 |
| --- | --- |
| T-001 | 申請表單 | 2 |

轉成 HTML 後會變成 <p>| ID | 功能 | 估算 | ...</p>。這是「表格莫名崩掉」的典型例子。

3. 儲存格內的 | 會拆欄

如果任務名稱包含像 A|B 這樣的 pipe,欄位就會在那裡被切開。用 \| 跳脫後,就能把 | 當成儲存格內容保留下來。

4. 不要相信 AI 的加總

語言模型在多位數加法或大量列彙總時,可能會出錯。而且即使錯了,它也會用很有自信的語氣回答。彙總結果一定要由機械式重新計算後比對

5. 「請幫我整理表格」時可能會刪掉列

請求 AI 幫忙整理或排序時,有時會掉列或改變順序。一旦讓它改了主表,就一定要用 git diff 檢查行數與差異。

請 AI 幫忙審查時的提示詞

fig_10_review_prompt.png

理解了這些坑之後,讓 AI 幫忙的重點就應該從 工時計算 轉向 表格結構審查。數字由我們自己掌握,請它看的是粒度與表記是否一致。

以下是可直接貼上使用的提示詞範例:

請幫我審查以下 WBS 表格。

目的:
- 希望能夠以功能軸與工程軸彙總工時
- 想減少作業粒度的落差
- 之後要轉成 CSV,再在 Excel 內做樞紐分析

請確認以下觀點:
1. 功能名稱的粒度是否一致
2. 工程名稱是否有表記不一致的情況(例如:單元測試 / 單體測試 / UT)
3. 任務是否為每行一個工作
4. 估算是否有特別大的列(哪些列應該拆分)
5. 狀態是否有 `todo` / `doing` / `done` 以外的值
6. 數值欄位中是否混入單位或符號
7. 儲存格內是否有未跳脫的 `|`

請輸出以下內容:
- 指摘事項(附上列 ID)
- 修正版的 Markdown 表格(不要減少列數,ID 不可變)
- 被修改的列清單

請不要修改工時數值本身。

最後兩行很重要。明確寫出 ID 不可變、列數不可減少,差異審查才成立。不讓 AI 改工時數值,並不是不信任它的估算,而是為了讓 AI 修改了什麼、自己修改了什麼,能夠在 git diff 中清楚區分。

也可以一起請它做彙總,但那些數字一定要用下一節的腳本再核對一次。

彙總驗算用的腳本

fig_11_verify_script.png

前面提到的第 1 到第 4 個坑,可以只靠 Python 標準函式庫的腳本機械式處理掉。將以下內容存成 wbs_rollup.py,並用 python3 wbs_rollup.py wbs.md 執行。

#!/usr/bin/env python3
"""讀取以 Markdown 表格撰寫的 WBS,並以功能軸與工程軸彙總工時。"""

import re
import sys
from collections import defaultdict

KEY_FEATURE = "功能"
KEY_PHASE = "工程"
KEY_ESTIMATE = "估算(人日)"
KEY_ACTUAL = "實績(人日)"

def split_row(line):
    """將一行 Markdown 表格拆成儲存格清單(不把 \\| 當分隔符)。"""
    line = line.strip()
    if line.startswith("|"):
        line = line[1:]
    if line.endswith("|"):
        line = line[:-1]
    cells = re.split(r"(?<!\\)\|", line)
    return [c.strip().replace("\\|", "|") for c in cells]

def is_delimiter_row(cells):
    return bool(cells) and all(re.fullmatch(r":?-{1,}:?", c) for c in cells)

def parse_first_table(text):
    """回傳本文中第一個 Markdown 表格的(標題、列資料、警告)。"""
    lines = text.splitlines()
    header, rows, warnings = None, [], []

    for i, line in enumerate(lines):
        if not line.lstrip().startswith("|"):
            if header is not None:
                break
            continue

        cells = split_row(line)

        if header is None:
            header = cells
            continue
        if is_delimiter_row(cells):
            if len(cells) != len(header):
                raise SystemExit(
                    f"第 {i + 1} 行:標題行({len(header)} 欄)與分隔行({len(cells)} 欄)"
                    "欄位數不同。此表將不會被 Markdown 正確渲染"
                )
            continue

        if len(cells) != len(header):
            warnings.append(
                f"第 {i + 1} 行:欄位數為 {len(cells)}(標題是 {len(header)}):"
                f"{cells[0] if cells else ''}"
            )
        cells = (cells + [""] * len(header))[: len(header)]
        rows.append(dict(zip(header, cells)))

    if header is None:
        raise SystemExit("找不到 Markdown 表格")
    return header, rows, warnings

def to_number(value, row_id, column, warnings):
    value = value.strip()
    if value in ("", "-", "—"):
        return 0.0
    try:
        return float(value)
    except ValueError:
        warnings.append(f"{row_id}: {column} 不是數字:{value!r}")
        return 0.0

def render_table(headers, rows, align_right=()):
    delim = ["---:" if h in align_right else "---" for h in headers]
    out = ["| " + " | ".join(headers) + " |", "| " + " | ".join(delim) + " |"]
    for row in rows:
        out.append("| " + " | ".join(str(c) for c in row) + " |")
    return "\n".join(out)

def fmt(value):
    return str(int(value)) if float(value).is_integer() else f"{value:g}"

def main(path):
    with open(path, encoding="utf-8") as f:
        header, rows, warnings = parse_first_table(f.read())

    for key in (KEY_FEATURE, KEY_PHASE, KEY_ESTIMATE, KEY_ACTUAL):
        if key not in header:
            raise SystemExit(f"找不到欄位 {key!r}。標題:{header}")

    by_feature = defaultdict(lambda: [0.0, 0.0, 0])
    by_phase = defaultdict(lambda: [0.0, 0.0, 0])
    cross = defaultdict(float)
    features, phases = [], []
    total_estimate = total_actual = 0.0

    for row in rows:
        row_id = row.get("ID", "?")
        feature, phase = row[KEY_FEATURE], row[KEY_PHASE]
        estimate = to_number(row[KEY_ESTIMATE], row_id, KEY_ESTIMATE, warnings)
        actual = to_number(row[KEY_ACTUAL], row_id, KEY_ACTUAL, warnings)

        if feature not in features:
            features.append(feature)
        if phase not in phases:
            phases.append(phase)

        for bucket, key in ((by_feature, feature), (by_phase, phase)):
            bucket[key][0] += estimate
            bucket[key][1] += actual
            bucket[key][2] += 1
        cross[(feature, phase)] += estimate
        total_estimate += estimate
        total_actual += actual

    print(f"## 彙總結果({len(rows)} 個任務)\n")

    print("### 功能軸\n")
    body = [[f, fmt(by_feature[f][0]), fmt(by_feature[f][1]), by_feature[f][2]]
            for f in features]
    body.append(["**合計**", f"**{fmt(total_estimate)}**",
                 f"**{fmt(total_actual)}**", len(rows)])
    print(render_table(["功能", "估算(人日)", "實績(人日)", "任務數"], body,
                       align_right=("估算(人日)", "實績(人日)", "任務數")))

    print("\n### 工程軸\n")
    body = [[p, fmt(by_phase[p][0]), fmt(by_phase[p][1]), by_phase[p][2]]
            for p in phases]
    body.append(["**合計**", f"**{fmt(total_estimate)}**",
                 f"**{fmt(total_actual)}**", len(rows)])
    print(render_table(["工程", "估算(人日)", "實績(人日)", "任務數"], body,
                       align_right=("估算(人日)", "實績(人日)", "任務數")))

    print("\n### 功能 × 工程(估算人日)\n")
    body = []
    for f in features:
        row = [f] + [fmt(cross[(f, p)]) if cross[(f, p)] else "-" for p in phases]
        row.append(fmt(by_feature[f][0]))
        body.append(row)
    body.append(["**合計**"] + [fmt(by_phase[p][0]) for p in phases]
                + [f"**{fmt(total_estimate)}**"])
    print(render_table(["功能"] + phases + ["合計"], body,
                       align_right=tuple(phases) + ("合計",)))

    if warnings:
        print("\n### 警告\n")
        for w in warnings:
            print(f"- {w}")
        sys.exit(1)

if __name__ == "__main__":
    if len(sys.argv) != 2:
        raise SystemExit("用法:python3 wbs_rollup.py <WBS Markdown 檔>")
    main(sys.argv[1])

這個腳本的重點不只是彙總,而是 偵測異常並以 exit code 1 失敗。例如把 T-012 那一行改成「估算寫成 3人日,並且多加一欄」後執行,會看到如下結果:

$ python3 wbs_rollup.py wbs.md
(…彙總結果…)

### 警告

- 第 14 行:欄位數為 9(標題是 8):T-012
- T-012: 估算(人日) 不是數字:'3人日'
$ echo $?
1

因為會透過 exit code 失敗,所以可以放進 CI。AI 修改 WBS 的 Pull Request 中,能自動攔下欄位崩壞與非數字儲存格。

# .github/workflows/wbs.yml 的一部分
- name: Validate WBS
  run: python3 wbs_rollup.py wbs.md

Git 中的運作流程

fig_12_git_flow.png

把前面內容落到實務上,流程會變成這樣:

  1. wbs.md(主表)放進 repository
  2. 任務新增與估算變更,由 AI 或人類直接編輯 wbs.md
  3. 執行 python3 wbs_rollup.py wbs.md,並將彙總結果輸出成 wbs_summary.md
  4. git diff 檢查變動行(行數是否符合預期、是否有不該消失的列)
  5. 做成 Pull Request,審查工時增減
  6. 在 CI 中執行 wbs_rollup.py,偵測表格崩壞

如果把彙總結果也保留下來,彙總本身的差異也能追蹤。

$ python3 wbs_rollup.py wbs.md > wbs_summary.md
$ git diff --stat
 wbs.md         |  2 +-
 wbs_summary.md | 12 ++++++------
 2 files changed, 7 insertions(+), 7 deletions(-)

「如果把 T-007 的估算從 5 改成 8,承認流程合計就從 16 變 19,整體從 47 變 50」這種因果關係,也會直接保留在差異裡。

轉成 CSV 後交給 Excel

fig_13_csv_export.png

最終報告常常還是要交給 Excel。在某些情況下,直接貼上 Markdown 表格也能匯入,但欄位一多就容易崩,所以透過 CSV 會更穩定。

wbs_rollup.py 一樣,這也可以只用標準函式庫完成。

#!/usr/bin/env python3
"""將 wbs.md 的表格轉成 CSV。用法:python3 wbs_to_csv.py wbs.md wbs.csv"""
import csv
import re
import sys

# 暫時保留跳脫過的 \|,使用不會出現在表格中的字元
SENTINEL = "\x00"

def split_row(line: str) -> list[str]:
    line = line.strip().replace(r"\|", SENTINEL)
    if line.startswith("|"):
        line = line[1:]
    if line.endswith("|"):
        line = line[:-1]
    return [c.strip().replace(SENTINEL, "|") for c in line.split("|")]

def parse(text: str) -> tuple[list[str], list[list[str]]]:
    lines = [ln for ln in text.splitlines() if ln.strip().startswith("|")]
    if len(lines) < 2:
        sys.exit("找不到表格")
    header = split_row(lines[0])
    if not all(re.fullmatch(r":?-{1,}:?", c) for c in split_row(lines[1])):
        sys.exit("第 2 行不是分隔行")

    rows = []
    for lineno, ln in enumerate(lines[2:], start=3):
        cells = split_row(ln)
        if len(cells) != len(header):
            # 這裡要直接失敗,先檢查欄位數不一致
            sys.exit(f"第 {lineno} 行:欄位數不是 {len(header)},而是 {len(cells)}")
        rows.append(cells)
    return header, rows

def main() -> None:
    src, dest = sys.argv[1], sys.argv[2]
    with open(src, encoding="utf-8") as f:
        header, rows = parse(f.read())
    # 給 Excel 用,所以用 utf-8-sig(含 BOM)。newline="" 是 csv 模組的慣例
    with open(dest, "w", encoding="utf-8-sig", newline="") as f:
        writer = csv.writer(f)
        writer.writerow(header)
        writer.writerows(rows)
    print(f"已將 {len(rows)} 行寫入 {dest}")

if __name__ == "__main__":
    main()

這個腳本刻意強調以下三點:

  1. 遇到欄位數不符就失敗:像 zip(header, cells) 這種簡單寫法,會在短的一側直接截斷,因此若不先檢查欄位數,很容易漏掉資料缺失。所以要在用 zip 組合前先驗證欄位數,只要有一行不符就停止。
  2. 還原 \| 跳脫:GFM 可以用 \| 在儲存格內寫 pipe。若單純 split("|"),欄位就會錯位。
  3. utf-8-sig 輸出:若在 Excel 以不含 BOM 的 UTF-8 開啟,在日文或中文環境下有時會被誤判成 Shift_JIS,導致亂碼。utf-8-sig 會在檔案開頭加上 EF BB BF 三個位元組,讓 Excel 直接把它當 UTF-8。反過來,如果這個 CSV 要再被程式處理,BOM 反而是多餘的,那就維持 utf-8 即可。

newline="" 是因為 csv 模組會自行以 r\n 輸出換行;不加的話,依環境不同可能會每隔一行多出空白行。

在 Excel 中做樞紐分析

fig_14_excel_pivot.png

匯入 CSV 後,就可以用樞紐分析表切換軸。這就像把 wbs_rollup.py 的同樣彙總,用 GUI 方式再做一次。

想看什麼列欄值功能別工時功能(不放)估算(人日) 的合計工程別工時工程(不放)估算(人日) 的合計功能 × 工程功能``工程``估算(人日) 的合計負責人別工時負責人(不放)估算(人日) 的合計估算與實績對照功能(不放)估算(人日)實績(人日) 的合計特別是把 功能 放在列、工程 放在欄的交叉彙總,可以一眼看出「只有這個功能的測試工時偏少」「設計工時太集中」這類偏差。

這裡也建議至少確認一次:Excel 的合計是否與 wbs_rollup.py 的合計一致。若不一致,通常是數值欄位混入單位或空白,導致 Excel 把它當文字處理。

整體流程會是這樣:

1. 用 Markdown 寫 wbs.md(主表)
2. 請 AI 做結構審查(不要讓它改工時數值)
3. 用 python3 wbs_rollup.py wbs.md 驗算
4. 用 python3 wbs_to_csv.py wbs.md wbs.csv 轉成 CSV
5. 匯入 Excel,使用樞紐分析表做彙總與圖表化

正本永遠是 wbs.md。如果在 Excel 裡直接修改,就會失去差異追蹤,所以要把 Excel 當成展示用檢視 來看待。

這種寫法不適合的情境

fig_15_limits.png

它不是萬能的。以下情況就應該老實考慮其他方法:

狀況會發生的事代替方案任務超過數百行表格太長,人與 AI 都難以讀完;AI 的上下文也會膨脹按功能分檔,或直接移到專用專案管理工具要管理依賴關係與關鍵路徑表格無法完整表現前後關係搭配甘特圖(例如 Mermaid 的 gantt)或專用工具多人同時更新衝突變多決定更新負責人與頻率,並固定行順序以降低衝突需要每天登記實績工時純靠 Markdown 手動輸入無法長期維持從工時管理系統匯入實績,反映到 實績(人日) 欄位想細緻管理進度狀態 欄位的粒度不夠把任務再拆小一點### 不要分得太細,也不要混得太雜

先不論適不適合,光是粒度就可能失敗。

「每行一個任務」很重要,但 如果拆得太細,更新表格本身就會變成工作。若細到數十分鐘一項,每次改估算都得動到幾十行,最後反而沒人維護。大致上,只有在符合以下任一條件時才拆:

  • 想彙總的單位(功能工程)改變了
  • 負責人變了
  • 估算太大,不拆的話精度不夠

反過來說,不要拆到連決策都用不到的粒度。

另一件事是,不要把估算用途與進度管理用途硬塞進同一張表。這張表的主要目的在估算,所以只保留 狀態實績(人日) 到某個程度即可。如果再加上開始日、結束日、議題、優先度、進度率等欄位,欄位會迅速膨脹,AI 和人都會更難讀,git diff 的雜訊也會增加。想更細追進度時,通常在那個階段改用別的工具比較健康。

若只是想輔助顯示依賴關係,也可以加上 Mermaid 的 gantt。Mermaid 會在任務名稱後用冒號分隔中繼資料,並以 after <taskID> 指定前置任務。

gantt
    title Expense Reimbursement App Renovation
    dateFormat YYYY-MM-DD
    section 申請表單
    需求定義 :done, t001, 2026-08-03, 2d
    設計     :done, t002, after t001, 3d
    實作     :active, t003, after t002, 5d
    section 核准流程
    需求定義 :done, t005, 2026-08-03, 3d
    設計     :done, t006, after t005, 4d
    實作     :active, t007, after t006, 8d

不過,這很容易變成甘特圖與 WBS 的雙重管理。工時彙總仍應以 wbs.md 為正本,而甘特圖則視為「展示用檢視」會比較穩妥。

總結

fig_16_summary.png

為了讓 WBS 更適合 AI 編輯,我做了以下事情:

  • 把 WBS 做成 每行一個任務的扁平 Markdown 表格,並用 功能 工程 欄位而非階層來承載軸
  • 將功能軸、工程軸、交叉彙總視為從主表 生成的檢視
  • 固定 ID,數值欄只放數字,且不要把合計列混進表裡
  • 依照 GFM 規範,當某行欄位過多時,多出的儲存格會被靜默丟棄;如果標題行與分隔行欄位數不同,表格甚至不會成立。必須準備能偵測 AI 編輯破壞的機制
  • 彙總由腳本重新計算,異常時以 exit code 1 失敗,並在 CI 中攔截
  • 請 AI 做的是 表格結構審查,並明確要求它不要改變 ID、不要減少列數、不要改數值
  • 要送進 Excel 時先轉成 CSV,並且 欄位數不一致的列要在這裡就失敗。為避免亂碼,以 utf-8-sig 輸出
  • git diff 能以行為單位追蹤工時增減

WBS 要不要交給 AI 的分水嶺,與其說是模型有多聰明,不如說是 「格式是否能察覺已經壞掉」。先把表格形式整理好,就更能放心把編輯工作交給 AI。

參考(官方資訊)


原文出處:https://qiita.com/Tadataka_Takahashi/items/6be2bf42e6b9751accc8


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝8   💬4   ❤️2
233
🥈
我愛JS
📝1   💬2   ❤️1
51
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登