逐层调试AI智能体:提示词、上下文、工具调用与循环结构
人工智能智能体故障的分层模型:提示词、上下文、控制机制与循环设计之间的差异,以及用于确定哪一层出现故障的以追踪为优先的排查流程。
当人工智能代理在实际应用中表现异常时,团队往往争论的是术语而非证据:有的工程师认为需要更好的提示词,有的则归咎于上下文问题,还有的指向框架本身。提示词设计、上下文管理、框架构建以及循环工程并非相互对立的理念,而是层层叠加的四个部分,每一部分都会以各自特有的方式出错。本指南通过简单的编程代理示例说明每个层次,随后提供一种以追踪为优先的流程,帮助你在修改任何代码之前确定究竟是哪个层次出了问题。
简版概述
- 提示词工程负责确定传递给模型的指令。
- 上下文工程决定模型在回答问题前能看到什么内容。
- 框架工程负责构建模型运行的环境:包括工具、内存、文件、权限以及故障恢复机制。
最昂贵的智能体故障往往源于对这些层级中错误部分的修复。
演示环境正常,生产环境却出问题
这种情况很常见:在演示中智能体表现完美,但上线几天后就开始陷入循环、消耗代币,忘记一小时前被告知的某些信息,或者一旦工具返回意外数据就会崩溃。
团队内部出现分歧:有人建议重写提示词,有人认为是上下文问题,还有人怀疑是框架的问题。由于缺乏统一的故障定位方法,原本两天的修复工作会变成两周的重新编写,因为每个补丁都应用在原本运行正常的层级上。
这就是将这四个术语区分开来的实际原因。每个术语都对应着问题可能出现的不同环节,一旦能够将它们区分开来,代理调试就会变成一种有章可循的过程,而非猜测游戏。
PACT:四层结构的助记法
在处理故障时,一种便于记住这些层的简洁方法就是PACT:Prompt、Awareness、Control、Trajectory。每个单词都对应一个问题:
- Prompt:任务要求是否明确?
- Awareness:模型是否获得了执行当前步骤所需的信息?
- Control:运行环境能否安全可靠地执行模型提出的要求?
- Trajectory:重复进行的流程是否正朝着可验证的结束状态前进?
目标并非增加另一层专业术语,而是让不同故障类型之间的界限清晰到足以让人在遇到紧急情况时能够迅速回忆并使用它们。
这些层次是如何形成的,以及顺序为何重要
随着模型不断获得新功能,这些术语也大致按顺序出现:
- 提示工程(大约2022年至2024年)是最初的技能:通过措辞、示例、约束条件以及少量样本模式来优化单次模型调用。
这些日期指的是相关术语开始被广泛使用的时刻,并非正式的里程碑,且在撰写本文时该领域的术语仍在发展中。重要的是,没有任何现有概念被取代——每一层都是建立在之前层次之上的,因为自主性的提升都需要相应的控制机制。
提示词工程:局部契约
提示工程是一门关于如何措辞、构建和阐明指令的技艺,以此让模型给出的响应更具可预测性。没有它,就会出现模糊的指令、不稳定的输出格式,还得让模型去猜测用户的真实意图。
在编程智能体中,代码审查提示可能如下所示:
SYSTEM: You are a code reviewer.
Given a diff, output ONLY valid JSON:
{"issues": [{"line": int,
"severity": "low|medium|high", "note": str}]}
No prose. No markdown fences.
If there are no issues, return {"issues": []}.
该提示设定了明确的规范:指定角色、描述转换过程(输入为差异对比,输出为问题列表)、精确规定输出的字段名称及允许的严重程度值,甚至还定义了空值情况,这样后续解析时就无需处理文字描述或缺失的键值。
有一种常见观点认为提示工程已经过时。事实并非如此,它其实是最核心的部分。每一个上下文处理流程最终都会向模型传递指令,而即便有出色的框架支撑,低质量的指令依然只能产生劣质结果,只不过现在这些结果被包装在令人印象深刻的基础设施之中罢了。
上下文工程:在预算限制下的筛选
上下文工程的职责就是为每次调用决定哪些文档、历史记录、工具定义和记忆内容会被纳入模型视野,同时同样精心地决定哪些内容不应被包含。如果忽视这一环节,模型就会依据过时的信息作答,被无关的检索内容带偏方向,或者因为视野中被塞满了以防万一的内容而失去重点。
用于编程智能体的上下文构建工具可能会先检索候选内容,对其重新排序,然后压缩历史记录:
def build_context(query, full_history, kb):
relevant = retrieve(query, kb, top_k=8)
reranked = rerank(relevant, query)[:3]
summary = summarize_if_long(
full_history, max_tokens=800
)
return {
"docs": reranked,
"history": summary,
"query": query,
}
可以将其视为一个漏斗结构。检索阶段会广泛筛选出八个候选项,再通过重新排序保留最相关的三个;而当对话历史过长时,才会将其压缩至大约800个标记。该函数返回的是结构化的小数据包,而非原始资料。这里所体现的能力是筛选、排序和压缩,而非处理海量数据。
这就是为什么最常见的误解——认为上下文工程就是为模型提供更多信息——往往是错误的。过大的上下文往往包含更多噪声:过时的指令、重复的事实以及相互矛盾的依据。大部分工作其实都在于决定哪些内容应该舍弃。
上下文工程的范围比RAG更广
检索增强生成是上下文工程中的一种技术,涉及检索环节。如上所述,一个RAG流程可能会提取八个信息块并重新排序为三个。上下文工程还包括压缩历史记录、格式化工具定义、保持关键任务状态、将验证结果反馈给模型,以及决定哪些内容可以省略。
即便没有向量存储的编码代理也依然面临真正的上下文问题需要解决。Git状态、打开的文件、编译错误、测试结果、当前计划以及之前操作的日志,所有这些都需要以可用的形式传递给模型。
利用工程学:意图与效果之间的界限
该框架是系统中与模型无关的部分:包括工具和文件访问、持久化内存、权限规则、沙箱环境、追踪功能,以及出现故障时的处理机制。如果没有一个良好的框架,要么得到的代理虽然能够进行合理推理却无法采取行动,要么虽然能采取行动但在工具调用出错时没有可靠的恢复方式。
一个工具调用封装器可以很好地体现这一理念:
def call_tool(tool_name, args, retries=2):
for attempt in range(retries + 1):
try:
result = TOOLS[tool_name](**args)
log_trace(tool_name, args, result, status="ok")
return result
except ToolError as e:
log_trace(tool_name, args, str(e), status="failed")
if attempt == retries:
return {"error": str(e), "recoverable": False}
args = repair_args(args, e)
该封装器并不试图直接解决问题,而是负责界定模型所要求的操作与实际系统执行的内容之间的边界。每一次尝试都会被记录下其参数、结果和状态。一旦发生故障,系统会使用修正后的参数进行有限次的重试;当所有重试都失败后,它会返回一个被标记为无法恢复的结构化错误信息,这样调用方就能获得可用于进一步推理的数据,而非导致程序终止的异常。
人们很容易将该工具集与所安装的智能体框架混为一谈。框架提供基础结构,而工具集则是你在其之上做出的具体决策:记录哪些内容、失败调用之后应如何处理、崩溃后何种状态得以保留、允许使用哪些指令,以及如何将执行结果反馈给模型。
循环工程:终止条件
循环工程定义了每次迭代的具体任务、智能体判断自身是否取得进展的方式,以及最重要的——停止运行的条件。其典型的失败情形就是无限循环:智能体不断调用工具并消耗令牌,因为从未有信号告知它任务已完成或已陷入僵局。
一个最简单的控制器结构如下:
def run_loop(task, harness, max_iters=15, stall_limit=3):
state = init_state(task)
stalls = 0
for i in range(max_iters):
action = plan_next_step(state)
result = harness.call_tool(action.tool, action.args)
state = update_state(state, result)
if is_goal_met(state):
return state, "success"
if made_no_progress(state):
stalls += 1
if stalls >= stall_limit:
return state, "stalled — escalate"
else:
stalls = 0
return state, "max iterations reached"
此处设置了多层限制。max_iters限定总迭代次数,is_goal_met用于判断是否达成目标从而决定退出,而停滞计数器则记录连续无进展的迭代次数,并在恢复进展时重置。当出现stall_limit次停滞后,循环会返回一个特殊状态,要求上级介入处理而非继续默默运行。每条退出路径都会返回明确的理由,便于事后对运行情况进行分析分类。
核心理念是终止合约:明确的目标、可量化的进度指标、有限的重试次数、时间和代币的预算限制、验证步骤,以及针对进度停滞时的处理政策。如需了解相同理念在 TypeScript 中的实现方式,请参阅用于 LLM 工具的受限智能体循环。
循环机制与工具封装常常被混为一谈。工具封装决定了智能体可以操作的内容以及单个工具层面的故障处理方式,而循环机制则位于其之上,负责决定逐轮的行为逻辑,包括整个运行过程何时应当结束。
四层结构并列展示
- Prompt(提示词):负责指令的设定。常见故障:模型误解意图或返回错误格式。首要检查点:输出结果本身。
- Context(上下文):负责为模型提供正确状态。常见故障:模型基于过时、缺失或干扰性信息作出反应。首要检查点:每轮的上下文快照。
- Harness(执行机制):负责任务的执行与恢复。常见故障:工具调用出错、盲目重试或无法恢复。首要检查点:工具调用记录中的失败项。
- Loop(循环机制):负责进程推进与停止控制。常见故障:智能体始终无法收敛或永远不停止。首要检查点:参数几乎完全相同的重复成功调用。
区分执行机制故障与循环机制故障
最能节省时间的观察方法是:两种不同的故障从外部看可能完全相同。由于控制器问题而陷入循环的智能体,以及因工具处理故障而停滞的智能体,都会表现出相同的症状:它们持续运行,费用不断上升,却没人知道原因。一份简短的检查清单就能将它们与其他故障区分开来。
1. 查看工具调用轨迹
如果所有调用都成功且结果合理,而智能体却几乎使用相同的参数反复调用同一个工具,那就说明存在循环问题。
2. 寻找重复出现的工具故障
如果某个工具一再出现故障,而智能体仍不改变策略地不断重试,那就可能是工具接口出了问题。
3. 检查模型所能看到的内容
如果在运行过程中模型似乎忘记了某个事实,需检查上下文构建:包括截断、替换、压缩以及状态在各轮次之间的传递方式。
4. 最后检查输出结果
如果工具运行正常、上下文准确且循环顺利结束,但答案仍然错误,则需查看提示词。
经验法则
循环错误出在关于下一步该做什么的决策上;利用错误则出在出现问题时的处理方式上;上下文错误在于模型能看到什么内容;提示词错误则出在你的请求本身。
在修改任何内容之前,先确定这四个问题中哪一种适用。
案例研究:一个不肯停止的拉取请求审核者
想象这样一个场景:一个团队部署了一个用于审查拉取请求的编码智能体。测试进行得很顺利。但在生产环境中,某个拉取请求的某些审查任务会持续超过40分钟,从而产生意外的费用。
第一种理论认为提示词过于模糊,导致智能体过度思考。于是重新编写了提示词,但问题依旧存在。第二种理论则与上下文有关:也许智能体在每一步都会重新读取整个代码库。但追踪记录显示并非如此——数据检索的范围是恰当的,只有相关的文件会被加载进来。
接着有人仔细查看了工具调用日志。该代理反复调用了其测试运行工具,每次调用都顺利完成且没有错误。实际上测试套件确实失败了,原因是某个与拉取请求无关的偶发集成测试出了问题。由于没有机制提示它已对该子目标进行了足够多的尝试,应当停止并上报问题,因此该代理不断试图通过修改无关代码和重新运行测试来解决这一故障。
这不是提示问题,也不是上下文问题,更不是框架问题;这些工具的表现完全符合设计要求。这纯粹是一个循环错误。通过逐步分析就能得出这一结论。
第一步:确认工具正常工作
日志中的成功执行记录排除了最简单的框架故障,比如命令从未运行或适配器持续抛出错误的情况。
步骤2:比较各迭代间的状态
测试结果存在于智能体的上下文中,且检索范围被正确限定,因此模型不会遗漏相关证据。
步骤3:检查是否收敛
每次迭代时代码库都会发生变化,但关键的验证指标却始终没有改善。该系统既没有停滞检测机制,也没有对同一子目标的尝试次数设置上限。
步骤4:让控制器学会检测停滞状态
解决方案是在控制器逻辑中加入一小段代码,用于比较各步骤之间的进展特征:
if progress_signature == previous_signature:
stagnant_steps += 1
else:
stagnant_steps = 0
if stagnant_steps >= 3:
escalate("no measurable progress")
stop()
progress_signature 可以是任何能够反映实际进展的信息,例如失败的测试项及其错误信息。如果该值没有变化,停滞计数器就会增加;当出现三次停滞后,控制器会带着原因升级并停止运行。任何实际的变化都会重置该计数器。
你还可以进一步限制针对每个失败签名的尝试次数,而不仅仅是总迭代次数。这种区分很重要:十五次有效的迭代可能完全没问题,但针对一个无法修复的测试失败进行十五次尝试则纯粹是浪费。真正的解决办法不是让模型变得不那么固执,而是为控制器提供停滞的明确定义。
案例研究:从上下文中消失的约束
现在想象这样一个智能体:在迁移开始时,它确定绝不能破坏现有的客户端。四十轮之后,对话内容被压缩,而该约束条件在摘要中丢失了。于是这个智能体提出了一个可能破坏现有系统的架构变更方案。
这看似是错误的推理,但问题出在别的地方。对比模型在不同轮次实际接收到的上下文即可发现端倪:如果该约束条件在第五轮存在,却在第四十轮消失了,那么解决办法就在于上下文设计——将关键不变量与对话内容分开存储,让摘要明确保留约束条件,并且不再把原始历史记录视为唯一的真实来源。
“模型忘记了”从来都不是完整的诊断结果。真正有意义的问题是:模型实际上被提供了什么信息。
分层优化,而非全面替换
每一项新兴技术都是在前人基础上的发展,而非取代它们。一个任务执行代理会将清晰的提示语包裹在精心挑选的上下文中,通过可靠的框架来运行它,并借助有计划的循环来控制整个流程。这些概念的演变反映了团队随着时间推移所需添加的内容:首先是更明确的指令,接着是经过更好筛选的信息,然后是为模型准备的执行环境,最后则是能够让该环境无人值守持续运行的控制器。如果您正在考虑是由自己的运行时系统还是框架来负责这个外部循环,运行时框架与组合式框架的比较可以帮您了解其中的权衡。
用PACT框架重新阐述:
- 提示语:定义指令内容。
然后将解决方案与故障对应起来。如果模型误解了明确指定的任务,则需要针对性优化;若模型缺乏所需事实,则需补充上下文信息;那些虽做出合理决策却未能转化为可靠行动的情况,需要通过机制设计加以解决;而那些在整体流程未收敛的情况下仍能成功的行动,则需通过循环处理来完善。
常见问题
“机制工程”不就是代理框架的另一种叫法吗?
不是的。框架提供的是构建模块,而“工具包”则包含你在组装这些模块时所做的决策:工具故障时的处理方式、状态持久化、日志记录、权限设置,以及如何将结果反馈给模型。
单问题助手需要循环设计吗?
其实不需要。只有当智能体在每个任务中需要执行多个操作,并且必须自行或通过控制器代码来判断工作是否完成时,循环设计才变得重要。单轮问答式的机器人几乎不需要或完全不需要设计循环。
你应该先学习哪一层?
从提示词与上下文设计开始,然后再学习工具包和循环结构。在调试运行时逻辑及控制器之前,你得先能区分糟糕的指令和缺失的状态。
一个错误会涉及多个层次吗?
是的,这类问题往往最难解决。上下文错误可能会掩盖循环中的进度信号,从而导致问题看起来像循环故障。应深入追踪问题的根源,而非仅修复最初显现的症状。
关键要点
- 这四个术语描述的是控制机制,而非相互冲突的趋势:指令、模型就绪状态、执行与恢复,以及带有停止规则的重复进度机制。
- 所有调查都应从追踪记录开始,而非提示信息:需了解模型看到了什么、运行时做了什么、各迭代之间发生了哪些变化,以及控制器为何选择继续执行。
- 使用几乎完全相同的参数反复成功调用的工具表明存在循环问题;而盲目重试却不断失败的则指向集成框架的问题。
- 应为循环明确定义“停滞”状态,最好依据具体的故障特征,并设定相应的升级处理路径。
相关阅读
- 模型上下文协议如何帮助AI智能体发现并调用工具 —— 对MCP的清晰解释:说明主机、客户端和服务器如何让AI应用发现工具,使用结构化输入调用这些工具,以及其局限性所在。
- CodeBuddy:为AI编码智能体提供更智能的上下文检索功能 —— 阐述了基于依赖图结构的上下文检索系统如何帮助AI编码智能体在大型代码库中避免上下文不足或过载的问题。
- 从编写 Kotlin 代码到指导智能体:移动工程师的新角色 — 编码智能体如何让 Android 工程师的工作更注重需求规范、上下文环境、架构限制与验证,以及哪些基础技能比以往更为重要。