目標先講清楚:
這篇整理我在 LangChain 里「工具(tool)」的 context 管理與防混淆做法:
- 用 結構化的工具描述 提升可讀性與可維護性;
- 以 InjectedToolCallId 避免並行呼叫結果對不上;
- 以 InjectedState 隱藏不必要參數,讓 LLM 看到「剛好就好」的輸入。
Prompt 設計好了,接下來就是 agent 與 agent、agent 與工具 的溝通。不同套件都有自己的工具機制;以 LangChain 為例,我們用 @tool
把 Python function 轉成 LLM 可呼叫的工具。
痛點常見在兩處:
以下做法可顯著降低上述風險。
description
與 parse_docstring=True
先把「何時用、怎麼用、注意事項」寫成 機器與人都能看懂 的說明,然後直接掛到 @tool
上。這能把 tool 規範 和 system prompt 分離,讓 system prompt 更乾淨、可讀可維護。
範例說明(可直接複用):
# WRITE_TODOS_DESCRIPTION
## When to Use
Multi-step or non-trivial tasks requiring coordination
When user provides multiple tasks or explicitly requests todo list
Avoid for single, trivial actions
## Structure
Maintain one list containing multiple todo objects (content, status, id)
Use clear, actionable content descriptions
Status must be: pending, in_progress, or completed
## Best Practices
Only one in_progress task at a time
Mark completed immediately when task is fully done
Always send the full updated list when making changes
Prune irrelevant items to keep list focused
## Progress Updates
Call TodoWrite again to change task status or edit content
Reflect real-time progress; don't batch completions
If blocked, keep in_progress and add new task describing blocker
## Parameters
todos: List of TODO items with content and status fields
## Returns
Updates agent state with new todo list.
掛上工具:
from langchain_core.tools import tool
WRITE_TODOS_DESCRIPTION = open("WRITE_TODOS_DESCRIPTION.md").read()
@tool(description=WRITE_TODOS_DESCRIPTION, parse_docstring=True)
def write_todos(todos: list[dict]) -> list[dict]:
"""Write or update a TODO list following the spec."""
# ... 你的邏輯(新增、更新、清理) ...
return todos
好處:
- 工具規格文件 即 工具的
description
,避免兩份文件走散。- system prompt 不再塞滿「工具使用規範」,清楚切分責任。
InjectedToolCallId
精準對應呼叫與結果多工具平行執行時,LLM 很容易把回應搞混。LangChain / LangGraph 提供 InjectedToolCallId:
tool_call_id
;tool_call_id
;流程:
1) LLM 輸出兩個呼叫:A1=write_todos(...)、B1=read_file(...)
2) 編排器平行執行兩工具,並把 "A1"/"B1" 注入到工具參數
3) 工具回傳時:ToolMessage(..., tool_call_id="A1") / ToolMessage(..., tool_call_id="B1")
4) 編排器據此把結果對回 A1/B1,避免交叉污染
重點:ID由系統注入,LLM只專注在調用工具的參數產生
這樣在高並發、多步驟時,結果永遠能對回正確的那一次呼叫。
InjectedState
隱藏肥大的 state工具只該暴露 與任務直接相關 的參數。把整包 state
丟給模型,既洩漏不必要資訊,也增加「幻覺」與誤用風險。
沒用 InjectedState 的樣子(不建議):
@tool
def write_todos(text: str, state: dict) -> list[str]:
...
# LLM 會看到:
# {"name":"write_todos","arguments":{"text":"買牛奶","state":{...超大 state...}}}
用了 InjectedState(建議):
from typing_extensions import Annotated
from langchain_core.tools import tool
from langgraph.prebuilt import InjectedState # 依版本命名可能不同
@tool
def write_todos(
text: str,
state: Annotated[dict, InjectedState()]
) -> list[str]:
...
# LLM 只會看到:
# {"name":"write_todos","arguments":{"text":"買牛奶"}}
# 真正的 state 由執行框架在底層注入,工具端仍可拿到。
這能 縮短提示字串、降低洩露風險、更容易對齊 schema。
當工具太多時,可以怎麼提供llm進行選擇