LLM 调用与 Agent 编排问题记录
Function Calling 不可信、Agent 会死循环、多 Agent 会互相坑——这些问题手册记录了实际的兜底方案。
问题一:Function Calling 输出幻觉参数,服务直接 500
现象:LLM 调工具时输出 "limit": "十"(中文数字)、漏必填字段、甚至编造不存在的工具名,Python 端解析直接抛异常。
原因:JSON Mode 只保证输出是合法 JSON,不保证字段名对、类型对、值合理。这是 LLM 的固有行为,必须工程兜底。
解决:五层兜底链,从强到弱逐级降级:
def parse_tool_call(llm_output, max_retries=3):
for attempt in range(max_retries):
try:
# 层1:JSON Mode 保证合法 JSON
# 层2:Pydantic Schema 验证(字段名/类型/必填 + 自动转类型)
call = ToolCallSchema.model_validate(llm_output["tool_calls"][0])
# 层3:工具名白名单 + 模糊匹配(容错拼写错误)
if call.name not in TOOL_REGISTRY:
matches = get_close_matches(call.name, TOOL_REGISTRY, n=1)
if not matches:
raise ValueError(f"未知工具: {call.name}")
call.name = matches[0]
return call
except ValidationError as e:
# 层4:把验证错误喂回 LLM 重试(第二次通常能自己纠正)
llm_output = retry_llm_with_error(e)
# 层5:降级为自然语言回复,不让用户感知 500
return None
关键认知:"limit": "十" 这类"类型可转换"的错误,Pydantic 自动转型能救;wikepedia 这类拼写错误,编辑距离模糊匹配能救;真正救不了的是"LLM 编造了不存在的字段"——只能重试 + 降级。
问题二:ReAct Agent 死循环,一晚上烧掉半个月预算
现象:Agent 连续 20 次调用同一个工具、同样的参数,token 一直烧。
原因:ReAct 循环的退出条件是"LLM 自己决定结束",但 LLM 可能陷入重复动作、或者一直说"我快好了"但永远不给最终答案。
解决:多重安全终止,按优先级检查:
def should_continue(state) -> str:
# 1. 硬性上限:步数、token、耗时 三选一先到先停
if state["step"] >= state["max_steps"]:
return "force_answer"
# 2. 幻觉检测:连续 3 次相同工具+相同参数,强制中断
calls = state["tool_calls"]
if len(calls) >= 3 and calls[-1] == calls[-2] == calls[-3]:
return "force_answer"
# 3. LLM 主动终止:输出 final_answer(唯一"正常退出"路径)
if state["last_msg"].get("final_answer"):
return "answer"
return "act"
兜底:强制终止后不要丢弃中间结果——让 LLM 基于 state 里已有的工具输出,生成一个"不完整但诚实"的答案,比报错重试体验好得多。
问题三:LangGraph 并发节点写状态,数据悄悄丢了
现象:用 Send() 派发 10 个并行检索任务,各自往 state 的 results: list 里 append,最后 state 里只剩 1 条。
原因:LangGraph 并发节点的状态合并按字段类型决定——普通字段是"后到覆盖先到",没有合并修饰器的 list 字段会被覆盖而不是拼接。
解决:给字段加合并语义:
from typing import Annotated
import operator
class AgentState(TypedDict):
# add 语义:并发节点的结果自动拼接,不丢
results: Annotated[list, operator.add]
# 普通字段:后写覆盖(适合单写场景)
final_answer: str
延伸:并发变慢的三种根因——CPU 密集任务(GIL 锁死,并发=排队)、LLM API 限流(并发触发 429)、大 State 的合并开销。并发不是银弹。
问题四:多 Agent 流水线,A 发了邮件 B 才发现不该发
现象:Agent A 写邮件并直接发送 → Agent B 审核发现问题 → 但邮件已经发出去了,不可回滚。
原因:副作用操作放在了流水线中间,前面的 Agent 执行了不可逆操作,后面失败无法补偿。
解决:把副作用从 Agent 内部抽到流水线边界——Agent 只产出"决策",最后一个节点统一执行:
错误架构:A(写+发邮件) → B(审核) → C(归档)
正确架构:A(产出草稿) → B(审核) → 执行层(审核通过才发送)
规则:所有"写外部系统"的操作(发邮件、调付费 API、改数据库)都收口到流水线末端;必须中途执行的,准备补偿操作(如邮件撤回 API)。
问题五:1 万人同时问同一个问题,LLM 配额被瞬间打爆
现象:热点事件导致相同 query 洪峰,每条请求都打 LLM,配额秒没,后面全部 429。
解决:请求合并(singleflight)——相同 query 只调一次 LLM,结果缓冲后扇出给所有订阅者:
const pending = new Map() // key: hash(query)
async function coalesce(query) {
const key = md5(query)
if (pending.has(key)) {
// 已有相同请求在生成 → 订阅等待
return pending.get(key).promise
}
// 第一个请求:发起生成,结果广播
const promise = callLLM(query).finally(() => pending.delete(key))
pending.set(key, { promise })
return promise
}
红线:合并 key 绝不能包含用户私有上下文,否则 A 的个性化结果会泄露给 B——只对"纯相同 query"合并,RAG 场景每人上下文不同就合并不了,要诚实接受这个边界。
小结
| 问题 | 一句话方案 |
|---|---|
| 幻觉参数 | Schema 验证 + 模糊匹配 + 重试 + 降级,五层兜底 |
| ReAct 死循环 | 步数/token/重复动作三重安全终止 |
| 并发状态覆盖 | Annotated[list, operator.add] 合并语义 |
| 副作用不可回滚 | 副作用收口到流水线末端统一执行 |
| 相同 query 洪峰 | singleflight 合并 + 结果扇出 |
