Agent 小知识:把动态 Prompt 做成一个系统组件

大多数人对 Prompt 的理解,它是一段提前写好的文字描述。在 AI 兴起的早期,包括现在,我们要让一个模型更好地分析并修复一段代码时,我们会写清角色、任务和几条要求。参考下面的示例:

Plain 复制代码
# 角色
你是一名专业的软件工程师。
# 任务
请分析下面这段代码,找出问题并修复。
# 要求
要求: 
1. 保持现有功能不变;
2. 补充必要的测试;
3. 给出修改说明。

对于一次性的任务(问答),这样一段固定指令就够用了。但当 LLM 进入到 Agent 的执行循环中时,情况就变了。一个 Coding Agent 要完成一次修复,要先理解项目结构、读取相关代码,调用搜索工具来定位问题,再修改多个文件后跑测试,最后再根据失败结果继续调整方向。这个过程中,每一个环节模型要面对的问题都不一样,一开始写好的那段固定 Prompt 根本覆盖不了整段任务。

而 Agent 真正依赖的是一份随着任务推进、不断重新组装出来的上下文。这就是本文今天要讲的动态 Prompt 拼装。

从固定文本到动态组件

传统的 Prompt Engineering 关心的是如何写清楚一句指令,让这一次的模型输出得更准。Agent 场景则关心在当前这个任务阶段,模型最需要看到哪些信息。

以修 Bug 为例,第一轮,模型要知道用户目标、项目背景和当前代码结构,先定位出来问题;第二轮,问题的候选范围缩小了,模型要看到相关文件的具体实现和错误日志;第三轮,已经修改完代码,模型需要测试结果和失败原因,好决定下一步动作。

如果每一步都把完整的项目背景、全部历史对话和所有工具信息一股脑塞进去,上下文会迅速膨胀;如果什么额外信息都不给,模型又没法继续往下做。因此,Prompt 从一段写死的文字描述,变成了按当前状态实时构造的系统组件。

Prompt 里的四类信息

一次 Agent 调用的输入,一般不是用户输入的某条单独消息,而是一段按结构拼出来的上下文:

Plain 复制代码
System Prompt
+ 任务目标
+ 当前状态
+ 相关上下文
+ 工具信息
+ 历史结果
+ 当前行动要求

上面这些信息的生命周期并不相同,我们可以大致可以分成这四类:

稳定信息,这类信息会长期存在、贯穿整个任务,包括不限于:Agent 的身份、行为规则、安全限制、输出格式。这种信息一旦确定,基本上不会随着执行过程变化:

Plain 复制代码
你是一名 Coding Agent。
修改代码前必须先理解现有结构。
完成修改后必须运行测试。

任务信息,这类信息的生命周期和一次任务绑定,任务结束就消失,像"修复登录接口中的 Token 过期问题"就属于此类信息。

状态信息,这类信息会随着 Agent 的执行不断刷新。在定位阶段,它可能是"当前发现 auth.py 中存在 Token 判断逻辑",改完代码后状态又变成了"已修改 auth.py,测试用例 test_login_expired_token 失败"。

外部信息,这类信息是临时加入的内容,按需加载,包括不限于:某个文件的内容、一次搜索结果、一段文档、一次工具返回。这种信息用完之后,往往可以移出上下文清出空间。

JavaScript 复制代码
Agent 输入上下文

┌────────────────┐
|  稳定信息       |
|  角色 / 规则    |
├────────────────┤
|  任务信息       |
|  用户目标       |
├────────────────┤
| 状态信息        |
| 当前执行进度     |
├────────────────┤
| 外部信息        |
| 文件 / 工具结果  |
└────────────────┘

而动态 Prompt 拼装的核心,就是在每一次模型调用前判断:这四类信息中哪些该进入当前输入,哪些不该被使用。

上下文膨胀与信息关注偏移

用户提问 - 模型回答 - 结束,这种普通聊天是一次性的。与它不同,Agent 运行在一个持续的循环里:理解任务 - 规划 - 调用工具 - 观察结果 - 调整计划 - 继续执行。每循环一次,系统都会产生新的信息,如果 Prompt 始终保持不变,会同时面临两个问题:

一个是上下文膨胀。一个 Coding Agent 刚开始执行任务时,只带有用户需求、项目说明和几个代码文件,执行几十轮之后,历史记录里堆满了所有的修改记录、所有的工具返回信息,以及所有的错误日志和执行过程记录。过去的信息占据了大量的 token,模型真正该关注的当前状态反而被稀释了。

另一个是信息关注偏移。Agent 最初关心的是"这个项目是什么",执行中会变成"这个函数为什么报错",最后又会变成"测试为什么失败"。每个阶段所需的上下文都不一样,一份写死的 Prompt 没办法跟着任务重心一起移动。

动态拼装就是为了解决这个问题,让每一次调用都只带上这一步真正需要的信息。

动态 Prompt 和 Agent Harness

绝大多数人会把模型之外的这层工程支架称作为 Agent Harness,并把它拆成几块相互配合的职责:上下文窗口管理、Prompt 架构、工具集成、编排、状态管理、错误处理。动态 Prompt 拼装属于其中的 Prompt 架构部分,它和上下文窗口管理是一种紧密协作的关系。

两者的分工是上下文窗口管理负责"预算怎么分",它要在有限的窗口里,决定要保留哪些信息、该压缩哪些信息,哪些信息又该被丢弃。动态 Prompt 拼装则负责"怎么组装",它要把这些被选中的信息,按结构拼成模型这一次调用真正读到的输入。前者管的是可见性,后者管的是组织方式,二者共同决定了 Agent 每一步的表现。

一个 Coding Agent 的三次调用

把上面的概念落到一次具体的修复任务中,Agent 三次调用的 Prompt 会明显不同。

在定位阶段,第一次调用 Prompt 只会给出角色、目标和项目背景,让模型先分析:

Plain 复制代码
角色:你是代码维护 Agent。
目标:修复用户反馈的登录异常。
项目:一个 Python Web 项目。
任务:分析问题原因。

搜索到相关代码后,第二次调用 Prompt 会补进定位结果和下一步指令:

Plain 复制代码
相关文件:auth/login.py
发现:Token 校验逻辑位于 validate_token()
错误:过期 token 被误判为有效。
下一步:分析该函数的实现。

改完代码后,第三次调用 Prompt 换成了测试反馈:

Plain 复制代码
已修改:auth/login.py
测试结果:2 个通过,1 个失败。
失败原因:数据库 Mock 数据缺少字段。
下一步:修复测试问题。

虽然上面三次模型调用对应的任务 Prompt 各不相同,但它们都会共享一套稳定信息(角色、规则等)。变化的是当前任务状态和外部信息,这些内容会随着 Agent 的执行过程不断更新。每次都重新拼装 Prompt 是 Agent 和普通聊天机器人在输入侧最直接的区别之一。

动态 Prompt 的工程实现

真正落地时,很少会有人去手写一个巨大的 Prompt 字符串,常见的做法是把它拆成可管理的模块,运行时再按当前任务组合:

Plain 复制代码
prompt/
├── system.md
├── role.md
├── rules.md
├── tools.md
├── memory.md
└── task_template.md

而组装逻辑本质上是一次模板渲染:

Plain 复制代码
final_prompt = render(
    system  = system_prompt,
    rules   = rules,
    goal    = current_task,
    context = retrieved_files,
    history = short_memory,
    tools   = available_tools,
)

到了这一步,Prompt 就从一段固定文本变成了 Agent 系统里一个有明确输入、可以被单独维护和测试的组件。

拆模块时,还有个和成本相关的细节要留意下,那就是拼装顺序。现在的推理服务大多都支持 Prefix Cache,相同的开头前缀可以复用计算结果、省去重算。而我们之前提到的稳定信息(System Prompt、规则、工具描述)恰好每步都不变,把它们固定放在最前面,让变化的状态和工具返回排在后面。前缀就能稳定命中缓存,省下延迟和费用;反之,如果每轮都要改动开头,缓存就整段失效了,成本也就上去了。

信息筛选的取舍

看到这里我们容易产生一个误解:既然 Agent 需要更多信息,那 Prompt 是不是越丰富越好?答案是否定的。上下文越长,Token 成本就越高,推理也会越慢,关键信息越容易被淹没,模型的注意力也会越分散。

好的动态 Prompt,每一步的关键动作都是在做减法。要及时移除已经用不上的信息,只保留当前目标、当前状态,以及这一步决策真正需要的那部分上下文。这和人工作时整理桌面很像------桌面越堆越满,并不会让手头的事更好办。

小结

在 Chatbot 时代,Prompt 更像一次性的提问。进入 Agent 时代,它变成了一个随任务持续变化的工作环境。真正稳定的 Agent 靠的不是一条写得极其精巧的超级 Prompt。而是它背后的状态管理、上下文筛选、工具反馈和信息组合,让模型在每一次行动时都只看到最相关内容。

下一期,我们将继续讲 Agent 的工具调用:工具为什么需要明确的输入输出契约,以及一个好的工具设计如何帮助 Agent 更稳定地完成任务。