Day 16:從 Prompt 到 Context Engineering — 輸入視窗是一份預算
今天進入主軸二:生成式 AI。前八天我們在解決關於 MLOps 的「模型會不會掛」,接下來八天解決「模型會不會智商下線」。
主軸一的讀者會發現,這邊的問題同樣需要工程紀律,只是換了一種形式。
今天要解決的問題
繼續一個在模型上線後的經典情境:
第 1 週:工程師寫了一句 prompt,效果不錯,上線。 第 3 週:發現某些情況答得不好,加了幾句規則。 第 8 週:prompt 長到 300 行,裡面有 27 條「注意:如果……就……」的規則,改一句話會讓另外三個情境壞掉,而且沒有人敢動它。
這就是「prompt 工程」做到極限的樣子。問題不在技巧不夠,在於把所有事情都塞進一段文字裡這個做法本身有上限。
今天要換一個框架來看這件事。
📊 職缺訊號
「Prompt 與脈絡設計」出現在 32.9% 的生成式 AI 職缺中。但要注意:幾乎沒有職缺是「只做 prompt」的——它總是與 RAG(48.1%)、Agent(47.6%)綁在一起。這說明市場已經過了「prompt engineer」這個職稱的階段,提示設計是一項基本功,不是一個職務。
一、現象:模型看到的不只是你寫的那段話
先建立一個基本認知:當你呼叫一次 LLM,模型實際收到的輸入是這樣組成的:

圖 16-1:模型輸入的六個來源(示意架構)。「prompt engineering」通常只處理第一項,而輸出品質由六項共同決定。
這就是 Context Engineering 與 Prompt Engineering 的差別:
- Prompt Engineering:怎麼把指令寫好。
- Context Engineering:在有限的 token 預算內,決定放什麼、放多少、放在哪個位置。
當你的系統開始有 RAG 與工具時,第二個問題的重要性會遠超過第一個。
二、原理:位置很重要——Lost in the Middle
如果脈絡是預算,那放在哪裡跟放什麼一樣重要:

圖 16-2:關鍵資訊在脈絡中的位置對答對率的影響(示意曲線,趨勢依 Liu et al. 2023 的公開發現繪製)。放在開頭或結尾的資訊被使用得最好,放在中段的最容易被忽略。
這個現象有兩個直接的工程結論:
- 不要為了「多給資訊」而把脈絡塞滿。 塞進去的第 15 份文件,可能反而稀釋了第 1 份文件的效果。這是 Day 19 要做重排序(reranking)的原因——與其給 20 份,不如給最相關的 3 份。
- 重要的東西放前面或後面。 實務上的常見安排:系統提示與規則放最前(也利於 prompt caching,Day 26)、檢索到的文件放中段但依相關度排序、使用者的問題與最關鍵的指令放最後。
三、動手:四個立即可用的技巧
3.1 弱 prompt 與強 prompt 的對照

圖 16-3:常用提示技巧與適用情境(示意對照)。技巧的效果高度依賴任務類型,沒有一招通用。
直接看實例。任務是「從客服對話中擷取退貨資訊」:
❌ 弱 prompt:
請幫我看這段對話有沒有要退貨,有的話告訴我。
問題:沒有定義「退貨資訊」包含什麼、沒有指定輸出格式、沒有處理「不確定」的情況。
✅ 強 prompt:
你是客服對話分析助理。請從以下對話中擷取退貨 相關資訊。
擷取欄位:
- order_id:訂單編號(格式為 A 開頭加 5 碼數字)
- reason:退貨原因,只能是 ["瑕疵", "不符描述", "不喜歡", "重複下單", "其他"] 之一
- urgency:緊急程度 1-3(3 為最急,客戶明確表達不滿或提到期限時為 3)
規則:
- 只擷取對話中明確提到的資訊,不要推測。
- 若某欄位無法從對話中確定,填入 null。
- 若整段對話與退貨無關,回傳 {"is_return_request": false}。
以 JSON 格式輸出,不要有其他文字。
<對話>
{{ conversation }}
</對話>
四個關鍵改進:定義了欄位與型別、用列舉限制了自由度、明確處理「不確定」與「不適用」、用標籤包住不可信的輸入(這也是 Day 25 防注入的基礎)。
3.2 讓輸出可解析:結構化輸出 + 驗證 + 重試
光在提示裡說「請輸出 JSON」是不夠的,生產環境要三件套:
"""結構化輸出的正確做法:約束 → 驗證 → 帶錯誤重試。"""
from pydantic import BaseModel, Field, ValidationError
from typing import Literal, Optional
import json
class ReturnRequest(BaseModel):
is_return_request: bool
order_id: Optional[str] = Field(None, pattern=r"^A\d{5}$")
reason: Optional[Literal["瑕疵", "不符描述", "不喜歡", "重複下單", "其他"]] = None
urgency: Optional[int] = Field(None, ge=1, le=3)
def extract(client, conversation, max_retries=2):
messages = [{"role": "user", "content": PROMPT.replace("{{ conversation }}", conversation)}]
for attempt in range(max_retries + 1):
resp = client.chat.completions.create(
model="...", messages=messages,
response_format={"type": "json_object"}, # 1) 用 API 層的約束
)
raw = resp.choices[0].message.content
try:
return ReturnRequest.model_validate_json(raw) # 2) 應用層驗證
except ValidationError as e:
if attempt == max_retries:
raise
# 3) 把錯誤訊息回饋給模型,讓它自己修
messages += [
{"role": "assistant", "content": raw},
{"role": "user", "content": f"輸出格式錯誤:{e}\n請依 schema 重新輸出。"},
]
第三步是關鍵:把驗證錯誤原文丟回去,模型的修正成功率相當高。這比自己寫正規表達式硬拆字串穩健得多。
