「將原始模型輸出轉化為結構化、可靠、可預測的行為」。 這是 prompt engineering 的工程定義。其中三個層次:

  1. 策略:zero-shot / few-shot / Chain-of-Thought — 用什麼方式引導模型推理
  2. 約束:JSON mode、schema、格式規範 — 讓輸出可被程式接住
  3. 曡代:用標註資料集量化準確率,像調程式一樣調 prompt

Context Engineering(上下文工程):重點不是單句 prompt,而是 「在有限 context window 裡,放對的範例、對的格式說明、對的歷史」。

一、三種 prompt 策略並排比較

這邊設計腳本對同一句話組出三種 prompt,逐一送模型,並示範各策略的輸出怎麼解析:

def chat(messages, temperature=0.0):
    """呼叫真實 LLM 取得文字回覆;無任何憑證則回傳 None,由呼叫端決定是否走 offline。"""
    kimi = _kimi_cfg()
    try:
        if kimi:
            from openai import OpenAI  # Kimi 為 OpenAI 相容 API
            cli = OpenAI(api_key=kimi["api_key"], base_url=kimi["base_url"], timeout=120)
            r = cli.chat.completions.create(model=kimi["model"], messages=messages, temperature=temperature)
            return r.choices[0].message.content
        if os.environ.get("ANTHROPIC_API_KEY"):
            import anthropic
            cli = anthropic.Anthropic()
            sys_txt = "\n".join(m["content"] for m in messages if m["role"] == "system")
            usr = [m for m in messages if m["role"] != "system"]
            r = cli.messages.create(model="claude-opus-4-8", max_tokens=1024,
                                    system=sys_txt or None, messages=usr, temperature=temperature)
            return r.content[0].text
        if os.environ.get("OPENAI_API_KEY"):
            from openai import OpenAI
            cli = OpenAI()
            r = cli.chat.completions.create(model="gpt-4o", messages=messages, temperature=temperature)
            return r.choices[0].message.content
    except Exception as e:
        print(f"⚠ 線上呼叫失敗,改用 offline mock:{e}", file=sys.stderr)
        return None
    return None

def build_prompt(task, strategy):
    """回傳 (messages, 說明) — 示範三種主流策略如何組 prompt。"""
    system = "你是情感分類器。只輸出標籤本身,不要解釋。"
    if strategy == "zero-shot":
        user = f"判斷情感(正面/負面/中性):\n{task}"
    elif strategy == "few-shot":
        ex = "\n".join(f"文本:{t}\n標籤:{l}" for t, l in FEWSHOT)
        user = f"依照範例判斷情感(正面/負面/中性):\n\n{ex}\n\n文本:{task}\n標籤:"
    elif strategy == "cot":
        user = (f"判斷以下文本的情感(正面/負面/中性)。\n"
                f"先用一句話分析語氣與關鍵詞,最後一行只寫『標籤:X』。\n\n文本:{task}")
    else:
        raise ValueError(strategy)
    return [{"role": "system", "content": system}, {"role": "user", "content": user}], strategy

執行策略看效果

python prompt_lab.py strategies --task "這家餐廳雷到爆,再也不來了"

任務文本:這家餐廳雷到爆,再也不來了

── 策略:zero-shot ──────────────────────────────
  送出 user prompt:
    判斷情感(正面/負面/中性):
    這家餐廳雷到爆,再也不來了
  模型回覆:負面
  解析標籤:負面

── 策略:few-shot ──────────────────────────────
  送出 user prompt:
    依照範例判斷情感(正面/負面/中性):

    文本:這家餐廳的服務超好,菜也好吃
    標籤:正面
    文本:等了一小時還上錯餐,超失望
    標籤:負面
    文本:還行吧,沒什麼特別的
    標籤:中性

    文本:這家餐廳雷到爆,再也不來了
    標籤:
  模型回覆:負面
  解析標籤:負面

── 策略:cot ──────────────────────────────
  送出 user prompt:
    判斷以下文本的情感(正面/負面/中性)。
    先用一句話分析語氣與關鍵詞,最後一行只寫『標籤:X』。

    文本:這家餐廳雷到爆,再也不來了
  模型回覆:分析:語氣強烈負面,關鍵詞「雷到爆」、「再也不來了」顯示極度不滿。
標籤:負面
  解析標籤:負面

觀察點:zero-shot 最省 token 但最不穩;few-shot 用範例鎖定格式;CoT 準度高但回覆需額外解析。

關鍵在這兩行 prompt

user = (f"判斷以下文本的情感(正面/負面/中性)。\n"
        f"先用一句話分析語氣與關鍵詞,最後一行只寫『標籤:X』。\n\n文本:{task}")

「先用一句話分析…最後一行只寫標籤」 這句話就是 CoT 的全部。它要求模型先推理、再下結論,而不是直接吐答案。這就是 Chain of Thought——讓模型把「思考過程」寫出來,而不是心算。

對照你的輸出,它確實發生了

模型的回覆分成兩段:

分析:語氣強烈負面,關鍵詞「雷到爆」、「再也不來了」顯示極度不滿。   ← 這是「思考鏈」(推理步驟)
標籤:負面                                                        ← 這是最終結論

第一行就是模型「想出來」的推理過程(chain of think),第二行才是答案。對比另外兩個策略:

策略 模型被要求做什麼 回覆長相
zero-shot 直接給標籤 只有 負面
few-shot 照範例給標籤 只有 負面
cot 先分析再給標籤 分析:…\n標籤:負面

程式碼唯一為 CoT 做的特殊處理

CoT 不是程式做的,是「叫模型自己一步步想」的一句提示詞做的。 程式碼只是配合它「答案在最後一行」的特性去解析。

76361-t4tjrx7vf9.png


三種不同策略的 token 消耗分析

策略 輸入 token 輸出 token 說明
zero-shot 最少 最少(1 個標籤) prompt 短,回覆只有 負面
few-shot 最多(塞了 3 個範例) 最少(1 個標籤) 範例佔滿 input
cot 中等 最多(分析+標籤) 逼模型多寫一段推理

CoT 貴在哪裏

貴在輸出端。看你剛纔的回覆:

分析:語氣強烈負面,關鍵詞「雷到爆」、「再也不來了」顯示極度不滿。   ← 多出來的 output token
標籤:負面

zero-shot 只要輸出 負面(約 1-2 token),CoT 卻要輸出整段分析(約 30-40 token)。這段推理文字全部按 output token 計費,而 output 單價通常是 input 的 2-4 倍。所以同一個分類任務,CoT 的單次成本可能是 zero-shot 的好幾倍。

這邊在 chat 下加入 token 獲取

r = cli.chat.completions.create(model=kimi["model"], messages=messages, temperature=temperature)
print(f"  token 用量:{r.usage}")   # prompt_tokens / completion_tokens / total_tokens
return r.choices[0].message.content

再執行一次可以看到


  token 用量:CompletionUsage(completion_tokens=3, prompt_tokens=59, total_tokens=62, completion_tokens_details=None, prompt_tokens_details=None)
── 策略:zero-shot ──────────────────────────────
  token 用量:CompletionUsage(completion_tokens=3, prompt_tokens=142, total_tokens=145, completion_tokens_details=None, prompt_tokens_details=None)
── 策略:few-shot ──────────────────────────────
  token 用量:CompletionUsage(completion_tokens=43, prompt_tokens=91, total_tokens=134, completion_tokens_details=None, prompt_tokens_details=None)
── 策略:cot ──────────────────────────────

實測 token 對照

策略 prompt(輸入) completion(輸出) total 貴在哪
zero-shot 59 3 62 兩邊都最省
few-shot 142 3 145 輸入非常多(塞了 3 個範例)
cot 91 43 134 輸出多很多(多寫了推理)

三個關鍵觀察

1. few-shot 貴在輸入,cot 貴在輸出——完全對稱

  • few-shot:prompt 59 → 142,多了 83 token,全是那 3 條範例撐大的。輸出照樣只有 3。
  • cot:completion 3 → 43,多了 40 token,全是「分析:語氣強烈負面…」那段推理。輸入只小漲。

2. total 看起來 few-shot 最貴,但實際帳單未必

單看 total,few-shot(145)> cot(134)。但計費是輸入、輸出分開乘各自單價的,而 output 通常貴 2-4 倍。假設 output 單價是 input 的 3 倍,用「等效 input token」重算:

策略 等效成本 = prompt + completion×3
zero-shot 59 + 9 = 68
few-shot 142 + 9 = 151
cot 91 + 129 = 220 ← 反而最貴

換算真實計價後,CoT 是最貴的那個,因為成本集中在單價高的 output 端。光看 total_tokens 會誤判。

3. completion=3 印證中文 token 很碎

負面 兩個字卻是 3 個 token——中文常常一字 ≥1 token,還可能加個結束符。不能單純用「字數」估 token 了。詳細要拆解token還必須看不同 llm 的 tokenizer 設計,通常也不一樣,只需要記得這件事即可。


二、JSON mode + 穩健解析 + 自動修復

接下來要用 schema 約束輸出,並寫出能容忍 ```fence ````、雜訊的解析器

python prompt_lab.py json-extract --text "張先生,0912-345-678,下週三下午三點看診"

def cmd_json_extract(args):
    schema = {"name": "姓名", "phone": "電話", "datetime": "預約時間"}
    system = "你是資訊抽取器,只輸出符合 schema 的 JSON 物件,不要任何多餘文字。"
    user = (f"從文本抽取欄位並輸出 JSON:\n"
            f"schema = {json.dumps(schema, ensure_ascii=False)}\n"
            f"找不到的欄位填 null。\n\n文本:{args.text}")
    messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]

    for attempt in range(1, 3):
        out = llm(messages, args.offline)
        data = extract_json(out)
        if data is not None:
            print(f"✓ 第 {attempt} 次解析成功:")
            print(json.dumps(data, ensure_ascii=False, indent=2))
            missing = [k for k in schema if k not in data]
            print("✓ 欄位齊全" if not missing else f"⚠ 缺欄位:{missing}")
            return
        print(f"✗ 第 {attempt} 次解析失敗,回覆為:{out[:80]!r}")
        # 自動修復:把錯誤回饋塞回對話,要求「只回 JSON」
        messages.append({"role": "assistant", "content": out})
        messages.append({"role": "user", "content": "你的輸出無法被 json.loads 解析,請只輸出合法 JSON 物件。"})
    print("✗ 兩次都失敗 — 真實情境下應記錄、降級或人工介入。")

# ─────────────────────────────────────────────────────────────────────────────
# 穩健 JSON 解析:容忍 ```fence```、前後雜訊,抓第一個完整物件
# ─────────────────────────────────────────────────────────────────────────────
def extract_json(text):
    t = text.strip()
    if "```" in t:  # 去除 markdown code fence
        t = t.split("```")[1] if t.count("```") >= 2 else t.replace("```", "")
        t = t[4:] if t.lower().startswith("json") else t
    s, e = t.find("{"), t.rfind("}")
    if s == -1 or e == -1:
        return None
    try:
        return json.loads(t[s:e + 1])
    except json.JSONDecodeError:
        return None

執行結果如下:

J:\git\mathTalk\class\AI-Engineer-Course\05-Prompt-Engineering>python prompt_lab.py json-extract --text "張先生,0912-345-678,下週三下午三點看診"
  token 用量:CompletionUsage(completion_tokens=38, prompt_tokens=109, total_tokens=147, completion_tokens_details=None, prompt_tokens_details=None)
✓ 第 1 次解析成功:
{
  "name": "張先生",
  "phone": "0912-345-678",
  "datetime": "下週三下午三點"
}
✓ 欄位齊全

在這裏可以看到 schema 的範本如下:

schema = {"name": "姓名", "phone": "電話", "datetime": "預約時間"}

抽取結果為合法 JSON,欄位齊全(name / phone / datetime)

不過當我胡亂修改内容時就會出現意料外的情況

python prompt_lab.py json-extract --text "張先生,0912-34512323-678,151234下週三下午三點看診"

token 用量:CompletionUsage(completion_tokens=41, prompt_tokens=114, total_tokens=155, completion_tokens_details=None, prompt_tokens_details=None)
✓ 第 1 次解析成功:
{
  "name": "張先生",
  "phone": "0912-34512323-678",
  "datetime": "下週三下午三點"
}
✓ 欄位齊全

實際上這個電話並不合理 "phone": "0912-34512323-678",

無標籤

關注作者:

新增評論