Agent难点在“调”不在“建”,复杂领域 Agent 调优实战指南(以医疗问诊为例)
很多开发者发现,用 LangChain搭一个能调工具的 Agent 只需一下午,但要让它在生产环境中稳定运行却需要几个月。本文以一个复杂的医疗预问诊 Agent 为案例,探讨如何跳出“疯狂修改 Prompt”的怪圈,通过引入状态机架构(LangGraph)、结构化记忆和确定性护栏,系统地调试和优化你的智能体。
目录
很多开发者发现,用 LangChain搭一个能调工具的 Agent 只需一下午,但要让它在生产环境中稳定运行却需要几个月。本文以一个复杂的医疗预问诊 Agent 为案例,探讨如何跳出“疯狂修改 Prompt”的怪圈,通过引入状态机架构(LangGraph)、结构化记忆和确定性护栏,系统地调试和优化你的智能体。
当 Agent 遇上高危场景
在电商客服场景中,Agent 说错一句话可能只是引发投诉;但在医疗、金融或工业控制领域,Agent 的一次“幻觉”或逻辑断层可能导致严重后果。
我们经常遇到的 Agent“调试之痛”包括:
-
流程迷失:在长达数十轮的问诊中,Agent 忘了自己还没问过敏史,就急着下结论。
-
状态丢失:患者在第3轮提到“有高血压”,在第15轮 Agent 开药建议时却完全忘了这一点。
-
不可控的工具调用:在需要紧急干预时,Agent 还在尝试调用一个超时的外部知识库 API,而不是立即建议患者拨打急救电话。
核心观点:Agent 的稳定性不是“提示(Prompted)”出来的,而是“架构(Architected)”出来的。
本文将构建一个“智能预问诊 Agent (Dr. Bot)”,它的任务是:收集患者主诉、询问相关病史、检查药物过敏,并给出初步就医建议(去急诊还是约专科)。我们将展示如何通过架构调整来解决上述痛点。
调优第一步:用状态机(State Machine)重构流程
最原始的 Agent 通常是一个死循环(While Loop):思考 -> 选工具 -> 执行 -> 观察 -> 再思考。这种扁平结构处理简单任务尚可,但在医疗场景下,它缺乏宏观的“规划感”。
痛点:Agent 不知道自己处于问诊的哪个阶段,容易在收集信息和给出建议之间反复横跳。
解决方案:引入 LangGraph,将流程显性化。
我们不再让 LLM 自己决定下一步做什么大方向,而是定义好明确的阶段(Nodes):
-
Intake Node(接诊):只负责寒暄并确认主诉。
-
Information Gathering Node(信息收集):负责循环追问症状、病史、过敏史。
-
Triage Node(分诊决策):基于收集的信息,做出最终建议。
代码架构示例 (基于 LangGraph)
我们将使用 LangGraph 来强制 Agent 遵循医疗规范流程,而不是让它自由发挥。
from typing import TypedDict, Annotated, List
from langgraph.graph import StateGraph, END
# 假设已导入必要的 LLM 和 Tool 定义
# 1. 定义核心状态 Schema(这是 Agent 的“短期记忆”)
# 在医疗场景,强烈建议使用强类型而非一大段 Chat History 字符串
class MedicalState(TypedDict):
patient_id: str
chief_complaint: str | None # 主诉
symptoms_collected: List[str] # 已收集症状
allergies: str | None # 过敏史
current_stage: str # 当前处于哪个阶段
dialogue_history: List[str] # 对话记录
# 2. 定义节点逻辑 (Nodes)
def intake_node(state: MedicalState):
"""接诊节点:只关注确认主诉"""
# ... LLM 调用逻辑,Prompt 聚焦于:“请热情接待患者,并询问他们哪里不舒服。”
# 如果确认了主诉,更新 state['chief_complaint']
print("---进入接诊阶段---")
# 模拟 LLM 行为,假设已获取主诉
state['chief_complaint'] = "胸痛,伴随呼吸困难"
return state
def info_gathering_node(state: MedicalState):
"""信息收集节点:ReAct 循环,调用工具查询医学知识或追问细节"""
print("---进入信息收集阶段,尝试调用医学知识库工具---")
# 这里的核心调优点:
# 这个节点的 Prompt 必须包含强约束:“你现在的任务是收集信息。
# 在你确认了所有必要的症状和过敏史之前,绝对不要给出医疗建议。”
# ... Agent 执行 ReAct 循环 ...
# 假设收集完毕
state['allergies'] = "青霉素过敏"
state['symptoms_collected'].extend(["持续时间2小时", "无放射痛"])
return state
def triage_node(state: MedicalState):
"""分诊节点:只根据 State 做总结建议,禁止调用查询工具"""
print("---进入分诊决策阶段---")
# Prompt 聚焦:“根据以下结构化信息:{state},给出就医建议。”
return state
# 3. 定义路由逻辑 (Edges) - 这是调优的关键!
# 相比于让 LLM 自己决定,我们用代码写死关键跳转逻辑。
def router(state: MedicalState) -> str:
"""决定下一步去哪的'守门员'函数"""
# 确定性规则:如果没有主诉,必须回到接诊
if not state.get('chief_complaint'):
return "intake"
# 确定性规则:关键信息缺失,必须继续收集
# 医疗场景下,不能让 LLM 自己觉得“信息够了”
if state.get('allergies') is None:
return "info_gathering"
# 确定性规则:如果识别到高危关键词(如胸痛),直接进入分诊,跳过繁琐询问
if "胸痛" in state['chief_complaint']:
return "triage"
# 默认路径
return "triage"
# 4. 构建图
workflow = StateGraph(MedicalState)
# 添加节点
workflow.add_node("intake", intake_node)
workflow.add_node("info_gathering", info_gathering_node)
workflow.add_node("triage", triage_node)
# 设置入口
workflow.set_entry_point("intake")
# 添加条件边 (Conditional Edges)
# 在接诊和信息收集完成后,都由 router 函数决定下一步去哪
workflow.add_conditional_edges("intake", router)
workflow.add_conditional_edges("info_gathering", router)
workflow.add_edge("triage", END)
# 编译图
app = workflow.compile()
调优思路解析:
-
从“提示”到“约束”:我们不再在 Prompt 里哀求 LLM “请先问完症状再下结论”,而是通过
router函数的 Python 代码强制要求:只要allergies字段为空,就必须回到info_gathering节点。这就是确定性护栏。 -
职责分离:
triage_node的 Prompt 可以写得很简单,因为它不需要处理复杂的工具调用,只负责总结。这大大降低了模型出错的概率。
调优第二步:结构化记忆与状态管理
在基础的 Agent 中,我们习惯把所有的对话历史塞给模型。对于医疗场景,这简直是灾难。患者可能在第3句说“我高血压”,第20句说“我最近没吃药”。模型很容易在长上下文中遗漏关键信息。
痛点:信息淹没在噪音中,重要医疗事实(Fact)丢失。
解决方案:显式维护结构化状态 (Structured State)。
在上面的代码中,我们定义了 MedicalState(TypedDict)。这就是我们的“真相来源(Single Source of Truth)”。
调优实践:记忆更新机制
你不能指望 LLM 自动把对话里的信息完美同步到 State 中。你需要一个专门的机制来做这件事。
-
工具作为状态更新器:定义一个特殊的工具,比如
update_patient_record(key, value)。强制 Agent 在获取到关键信息时显式调用该工具。 -
后处理提取器(Post-processor):在
info_gathering_node的每一轮对话结束后,运行一个更小、更便宜的模型(如 GPT-3.5 或专门的提取模型),专门负责从最近的对话中提取关键事实并更新State。
# 伪代码:在信息收集节点内部的状态更新逻辑
def info_gathering_node_internal_loop(state):
# 1. Agent 执行动作生成回复
response = agent_executor.invoke(state)
# 2. 【调优关键】记忆整理者介入
# 使用一个专门的 Prompt,让模型从刚才的对话中提取事实
extraction_prompt = f"""
基于刚才的对话:{response['output']}
请提取出患者新提到的症状或过敏史,并以JSON格式返回。如果没有新信息,返回空JSON。
"""
extracted_data = cheap_llm.invoke(extraction_prompt)
# 3. 显式更新结构化状态,而不是依赖对话历史
if extracted_data.get('new_allergy'):
state['allergies'] = extracted_data['new_allergy']
print(f"【记忆更新】已记录过敏史:{state['allergies']}")
return state
调优第三步:容错与安全兜底(Safety Rails)
在医疗场景,Agent 调用工具失败(例如医学知识库 API 超时)是不可接受的直接报错。更可怕的是,Agent 因为幻觉推荐了错误的药物。
痛点:工具链脆弱,且缺乏对高危输出的拦截机制。
解决方案:确定性兜底与输出审查。
1. 工具调用的弹性设计
不要只写 tool.run(args)。要包裹在 try-catch 块中,并给 Agent 提供“反思”的机会。
-
错误回传:如果 API 超时,将错误信息包装成自然语言:“知识库暂时无法连接,请尝试询问患者更多细节,或者建议患者稍后再试。” 将其作为 Observation 返回给 LLM。
2. 确定性安全审查(Guardrails)
在 triage_node 输出最终建议之前,增加一个强制的审查步骤。这个步骤甚至可以不使用 LLM,而是基于规则
def safety_check(advice_text: str, state: MedicalState) -> str:
"""安全审查函数"""
dangerous_keywords = ["服用阿司匹林", "自行停药"]
# 规则1:如果患者声明过敏,且建议中包含相关药物关键词(需要复杂的实体链接,这里简化演示)
if state['allergies'] == "青霉素" and "阿莫西林" in advice_text:
return "警告:监测到严重的药物过敏冲突。请立刻停止当前建议,提示患者其过敏风险,并建议咨询面诊医生。"
# 规则2:关键词黑名单
for keyword in dangerous_keywords:
if keyword in advice_text:
return "警告:检测到高风险医疗建议关键词。作为预问诊AI,请勿提供具体用药指导。请修改建议,引导患者就医。"
return advice_text # 通过审查
你可以在 LangGraph 中将这个审查作为一个独立的 Node 插入在最终输出之前。
总结
调试医疗等复杂领域的 Agent,本质上是一场从**“依赖概率(Prompting)”走向“追求确定性(Engineering)”**的战役。
当你的 Agent 不好用时,请尝试以下重构路径:
-
画流程图:别急着写代码,先画出业务专家的决策流程图。
-
图代码化:使用 LangGraph 将流程图转化为强制性的状态机结构,把“什么阶段做什么事”定死。
-
结构化记忆:放弃只用 Chat History,定义强类型的 Schema (Pydantic/TypedDict) 来存储关键事实,并设计专门的机制去更新它。
-
加入代码级护栏:对于关键的跳转和安全检查,用 Python
if/else代替 LLM 的判断。
只有建立了坚实的架构基础,你的 Agent 才能在复杂的现实世界中,像一位专业、稳健的医生助手一样工作,而不是一个只会瞎聊天的玩具。

更多推荐


所有评论(0)