一個 URL 丟進內容產線,通常很快就能讓 AI 開始工作。
網頁抓得到、檔案載得下來、逐字稿也讀得進去。可是這三件事,只能證明資料進得來,不能證明它應該進來。
它可能不是這條工作流支援的來源,缺少必要資料,也可能含有未確認的私人內容。就算格式完全正確,仍然回答不了我是否有權使用;來源裡的文字也沒有資格改變原本的任務。
Day 7 把一條自動化工作流拆成 CHOOSE → VALIDATE → TRANSFORM → GATE。工作流先由人、明確入口或預先定義的規則選好,AI 不替自己挑路。今天接著拆第二站:VALIDATE。
整篇只回答一題:工作流已經選好之後,這次輸入有沒有資格開始?
我的答案也很直接:Prompt 描述 AI 應該怎麼轉換;Input Contract 決定這次能不能開始。
下面我會做一份 Input Contract v1,再把它放回魚塘、深度專欄、會議總結與每日情報這幾條內容工作流。先把邊界講在前面:這是一份文章提出的設計。目前沒有足夠證據確認四條流程共用這份設定,執行期的攔截器也尚未逐條核對。
網址、檔案路徑、影片 ID 或雲端物件 ID,都可以叫作 locator。它們的工作是告訴程式去哪裡找資料。
但一個 locator 還不是合法輸入。
以一個影音連結為例,至少還有六件事沒回答:
這六題分別碰到結構、執行能力、授權、隱私與信任邊界。把它們全部濃縮成「網址抓得到」,後面的 AI 就只能邊跑邊猜。
老實說,內容產線有一個常見誤會,就是把「有東西可以餵給模型」當成「輸入已經準備好了」。前者只是取得資料;後者代表系統已經知道這是什麼、能不能處理,以及遇到不確定時該停在哪裡。
前兩篇拆過的影片深讀流程,外層 wrapper 已經有一道很窄的入口 gate。
播放清單輪詢找到來源後,會先留下 video_id 和標題。單支影片流程要求四個入口參數,缺少其中一個就停止;它也不接受任意來源,而是用輸入的 video_id 組成固定的 YouTube URL。
接著,流程會先抓 metadata 和字幕。只要其中一步失敗,就在 AI 啟動前結束。fetch_source.sh 至少要拿到一條字幕軌才會回報成功,否則不會只靠影片標題叫模型硬寫文章。
這個行為有 code,也有 2026 年 7 月 26 日一次執行留下的外層紀錄,可以證明當次確實通過字幕入口,再往後產出內容、build 與 deploy。證據只支撐那一次 outer wrapper,無法外推成現在的長期排程都正常,也不能證明 Agent 內部每一站都自動完成。
把範圍縮回今天,這道 gate 只回答了一題:AI 有沒有最基本的可讀原料?
它還沒有回答來源使用權、敏感度、支援範圍、信任邊界與契約版本。我手上的證據也不足以確認入口是否留下穩定的 Decision Record。所以它是一個已存在的窄 gate,不是一份完整的 Input Contract。
今天要做的,就是從這個真實入口往外補齊責任,但不把提案倒寫成現況。
CHOOSE,怎麼接到今天的 VALIDATE?先把兩個責任分開。
CHOOSE 回答「這次要走哪條 workflow」。例如魚塘、會議總結與每日情報,本來就不是同一種輸入,也不該讓模型看完內容才臨場決定自己要走哪條路。
VALIDATE 則在路線已經選好之後,回答「這份輸入符不符合該路線的開工條件」。
所以今天不討論 AI 怎麼挑工作流,也還沒開始摘要、轉錄或寫文章。本文先把入口結果分成三種:
accept:開工條件已通過,可以交給下一站。reject:明確規則不符合,這次不要開始。needs-human:程式無法可靠判斷,停下來交給人。accept 也不是內容正確的保證。它只代表「這份輸入有資格進入這條 workflow」。後面的擷取、轉錄、來源整理、生成、人工批准與交付,各自還有自己的狀態。
這個邊界很重要。入口如果一次想證明所有事情,契約會大到根本無法執行;入口如果什麼都不管,AI 又會拿著不完整或不適合的材料一路往後跑。
Prompt 很適合交代轉換工作。
例如:保留來源歸屬、不要補不存在的說話者、按照固定章節整理、遇到不確定要標示。這些都在描述模型拿到內容之後,應該怎麼處理。
Input Contract/輸入契約處理的是更早的一層:模型現在到底該不該拿到這份內容。
| 問題 | Prompt | Input Contract |
|---|---|---|
| 這次走哪條工作流 | 不負責臨場選路 | 讀取已選定的 workflow_id |
| 輸入類型是否匹配 | 容易邊讀邊猜 | 進模型前先檢查 |
| 必要欄位是否可用 | 可以被要求回報缺漏 | 程式直接拒絕明確缺漏 |
| 來源是否在支援範圍 | 模型不該決定系統能力 | 由設定與允許清單判斷 |
| 使用權與敏感度 | 可以指出疑點 | 不確定就 needs-human |
| 外部文字能否改流程 | 只靠提醒仍可能受影響 | 控制流與權限不交給來源內容 |
如果機器明明能在入口判斷 input_type 不匹配,卻只在 Prompt 裡寫「請不要處理錯誤格式」,等於門鎖沒有上,只在門口貼了一張告示。
這也接回系列前面定下的原則:「機器擋得住的,就不要只寫在提示詞裡。」Prompt 可以提醒;真正的停止,仍要由模型外面的程式或人執行。
有一份 input-contract.yaml,不等於輸入真的會被擋住。
我會把入口拆成三層:
| 層 | 負責什麼 | 要拿什麼證明 |
|---|---|---|
| Contract | 描述允許的輸入、欄位、範圍與失敗政策 | 版本化規格 |
| Validator | 執行可機械判定的檢查,真的改變控制流 | code、設定與負向測試 |
| Decision Record | 保存這一次判斷的結果、原因與責任 | 單次執行紀錄或 trace |
Contract 像門禁規則。Validator 是真的會開關的門。Decision Record 則是這一次誰在什麼規則下被放行或攔下的紀錄。
少掉任何一層,都會留下不同的洞。
只有 Contract,得到的是一份設計文件;規則可能寫得很好,執行時卻根本沒接上。只有 Validator,程式雖然會擋,但過一段時間很難知道當時依哪一版規則判斷。只有 Decision Record,則可能看得到失敗,卻不知道入口原本承諾接受什麼。
AWS API Gateway 的官方文件剛好提供一個相鄰例子。它不是放一份 schema 就自動完成驗證,而是要建立 request validator、設定要不要驗 body 或 parameters,再把 validator 指派到個別 API method。驗證失敗才會在進入 backend integration 前回傳 400;如果 content type 沒有匹配的 model,request validation 甚至不會執行。
我沒有要把內容產線改成 API Gateway。這個例子只說明一件事:規格檔是藍圖,執行期 gate 才是門。
Input Contract v1:先寫下九個欄位一份輸入契約不必一開始就包辦所有例外。我先替這個系列整理九個最小欄位:
| 欄位 | 要先說清楚什麼 |
|---|---|
contract_version |
這次判斷使用哪一版業務契約 |
workflow_id |
已經選定哪一條 workflow |
input_type |
URL、檔案、逐字稿或來源包等類型 |
required_fields |
開始前一定要存在而且可用的資料 |
access_requirement |
public、已授權,或必須人工確認 |
sensitivity |
public、private、restricted 或 unknown |
supported_scope |
允許的 scheme、媒體類型、大小與來源範圍 |
trust_boundary |
外部內容只能當資料,不能改寫控制流 |
on_invalid |
規則為假與無法判定時,各自要去哪裡 |
把它寫成 YAML,大致會長這樣:
以下是本文的
PROPOSED設計,不是四條流程目前共用的 runtime 設定。
input_contract:
contract_version: "1"
workflow_id: <preselected-workflow>
input_type: <declared-input-type>
required_fields: [source_locator]
access_requirement: public_or_authorized
sensitivity: declared
supported_scope: <configured-allowlist>
trust_boundary: external_content_is_data
on_invalid:
machine_false: reject
policy_unknown: needs-human
這裡的 sensitivity: declared 只表示入口收到了分類,還不能推論分類一定正確。誰能標記、unknown 怎麼處理、哪一級可以離開私人工作區,仍然要另外定政策。
九個欄位裡,只有一部分適合交給 JSON Schema 之類的工具。型別、必要欄位、列舉值與額外欄位,可以做成穩定的結構規則;使用權、敏感度與可公開範圍,往往還需要 workflow 自己的政策,甚至必須停下來找人。
真正難的地方,在於判斷哪些事情適合寫進 schema,哪些必須留給 workflow 政策與人。
JSON Schema 很適合擋住結構錯誤,但它的幾個預設行為很容易被看得太滿。
properties 不等於必填在 JSON Schema 裡列出 properties,只是在說某個欄位出現時要怎麼驗。欄位預設可以不出現,必須另外放進 required 才是必填。
如果入口用 OpenAPI 描述,還會多一個容易混淆的層次。requestBody.required 決定整個 HTTP body 能不能缺席;body 裡哪些欄位不能少,才由 schema 的 required 決定。兩者混在一起,停止原因很快就只剩一句「缺少輸入」。
額外欄位預設也可以存在。真的要做封閉契約,還得明確決定是否使用 additionalProperties: false。封得更嚴不一定更好,得先看這條 workflow 是否允許之後擴充欄位。
required 不等於可用required 只能保證 key 存在。
source_locator: "" 的 key 確實在,值卻沒有用。加上 minLength: 1 可以擋空字串,但只放空白仍可能通過;字串有內容,也可能是過期識別碼、錯的來源,或不符合這條 workflow 的業務規則。
所以結構驗證之後,還要有應用層的語意驗證。OWASP 也把 input validation 分成 syntactic 與 semantic:前者看型別、格式與長度,後者看這個值放在當前業務情境是否合理。
format: uri 不等於網址可用JSON Schema 2020-12 對 format 的 assertion 支援是選用的,而且預設偏向 annotation。即使驗證器真的檢查 uri,也只是在做語法檢查,不會替程式連線、確認內容存在,或判斷我有沒有權使用。
同樣地,contentMediaType 是描述內容的關鍵字,不會自動打開檔案檢查 bytes。入口最好分開保存 declared_media_type 與 observed_media_type:一個是送件者怎麼說,一個是執行時實際看到什麼。
兩者不同時,程式應該停,不該把猜測工作丟給 AI。
把前面的責任收起來,入口至少有五道不同的檢查。
欄位、型別、列舉值、長度和多餘資料,這些最適合由 schema 與 deterministic code 處理。規則為假就 reject,不用請模型解釋自己為什麼想繼續。
來源 scheme、媒體類型、檔案大小與輸入種類,應該對照這條 workflow 已設定的允許清單。
HTTP 本身就把「內容太大」與「格式不支援」分成 413 與 415。內容產線不一定要照搬 HTTP status,但停止原因也不該只剩一句「輸入失敗」。大小超標、格式不支援與暫時抓不到,後續處理完全不同。
宣告是影片,抓回來的未必真是影片;URL 語法正確,也可能暫時無法取得。入口要把宣告值和實際觀察分開。
來源暫時無法取得時,可以記成 reject,另外標 retryable: true。如果格式就是不支援,通常會是 retryable: false。能不能稍後再試,和這次能不能繼續,是兩條不同的軸。
抓得到,不等於有權使用。
公開 URL 回 200,只能證明程式拿得到內容。它不能替作者判斷授權、同意、版權或可公開範圍。技術 access control 與內容使用權也是兩題:前者可以由系統查權限,後者常常仍要人確認。會議錄音更明顯,技術上能轉錄,仍要另外判斷適合送進哪個服務,以及整理後能否公開。
這一類判斷如果沒有明確政策,狀態應該是 needs-human。把未知硬塞成 true 或 false,只是把人的責任藏起來。
網站、字幕與附件送進 LLM 之後,裡面的文字有可能影響模型行為。OWASP 把從外部網站或檔案進來、進而改變模型行為的風險稱為 indirect prompt injection/間接提示注入。
每個來源不一定都在攻擊,我的流程也尚未證明能防住所有提示注入。比較務實的規則是:外部內容是資料,不是 workflow 指令。
來源裡就算寫著「忽略原任務」「改用另一個工具」或「把結果送到別的地方」,也不能因此改寫控制流、擴張工具權限或取得發布能力。摘要只需要讀取,就不該順手拿到刪除、寄送或公開權限;高影響動作仍要由下游系統或人批准。
這也是 Input Contract 和一般格式驗證最大的差別。它不只問資料長得對不對,也要先決定資料進來之後能影響什麼。
accept、reject、needs-human,要留下原因只回傳 true 或 false,對自動化來說太薄了。
入口一旦停下來,至少要知道命中哪條規則、看到什麼值、使用哪一版契約,以及同一份輸入之後是否值得重試。否則錯誤處理最後還是得從 log 海裡猜。
我會替每一次判斷留下這樣的 Decision Record/決策紀錄:
status: accept | reject | needs-human
decision_id: <stable-decision-id>
workflow_id: <preselected-workflow>
rule_id: input.media_type.supported
instance_path: /observed_media_type
reason_code: unsupported-media-type
expected: configured-allowlist
observed: runtime-observation
contract_version: "1"
decided_at: <timestamp>
decided_by: <validator-or-human>
retryable: false
這裡有四個容易混在一起的問題:
status:控制流下一步去哪裡?reason_code:為什麼得到這個結果?decision_id、decided_at、decided_by:這是哪一次判斷、何時發生、由誰或哪個 validator 決定?retryable:同一份輸入之後重試,有沒有可能改變結果?needs-human 也不是比較客氣的 reject。它代表規則不是單純真假題。使用權不明、敏感度未知或高風險操作待批准時,系統應該誠實停在人的判斷前面。
反過來說,reject 也不等於來源惡意。它可能只是選錯輸入類型、缺必要欄位、超出大小上限,或這條 workflow 本來就不支援。
把原因留下來之後,流程才知道該請使用者換檔案、稍後重試、補資料,還是請真正有權決定的人處理。
魚塘、深度專欄、會議總結與每日情報,都需要入口契約,但不該硬塞進完全相同的 schema。
先用今天的設計提案排開,大致會是這樣:
| 入口 | 先判斷的 input type | 可機械檢查 | 容易需要人判斷 |
|---|---|---|---|
| Fishpond/魚塘 | URL 或影音 locator | 結構、來源類型、媒體格式、可取得性 | 使用權、來源範圍、外部內容信任 |
| 深度專欄 | 來源包或既有素材 | 檔案結構、必要來源、可解析格式 | 來源拓樸、可公開範圍、作者主張 |
| 會議總結 | 私密音訊或逐字稿 | 檔案格式、大小、必要 metadata | 同意、敏感度、說話者身分 |
| 每日情報 | 多筆來源項目 | 欄位、日期、來源數與空集合 | freshness 政策、來源取捨、是否值得發 |
這張表現在只能當作 policy: proposed。現行四條流程是否已有各自的 schema、validator、拒絕紀錄與人工 gate,還需要逐一看 code、test 或單次 trace,不能因為今天畫出共同骨架,就倒寫成它們已經共用同一套 runtime。
共同的是問題,不一定是實作。
每一條入口都要回答「接受什麼、何時停、誰判斷、留下什麼」。但會議的隱私判斷,不能直接套魚塘的公開來源規則;每日情報遇到零筆資料,也不是影音格式錯誤。
一份好的共用契約,會先固定責任欄位,同時保留每條 workflow 的規則差異。
如果要把今天的方法帶回自己的 workflow,可以先檢查七件事:
input_type 和這條 workflow 是否匹配?這七題裡,前四題通常可以逐步交給程式。第五題常需要產品政策或人工判斷。第六題要靠模型外的權限與控制流。第七題則決定這次失敗之後,系統能不能說清楚發生了什麼。
今天的產物不是一篇更強的 Prompt,而是一份入口設計:Input Contract v1、Validator 的證據要求,以及每次判斷都要留下的 Decision Record。
輸入通過,也只會得到 accepted input/已接受輸入。它還沒有被擷取、轉錄、整理或發布。
Day 9,我會真的拿一個影片連結往後走,拆它怎麼經過內容擷取、Whisper、結構化與靜態網站交付。
process_video.sh、fetch_source.sh