Day 08 / 共 20 天 · 第 2 周 Agent 执行

提示词构建

智能体每一步都要把 role/goal/工具/记忆拼成一段 LLM 提示。今天读提示词构建的两级流程,尤其是"滚动记忆压缩"这个应对长对话的关键机制。

📍 你在整门课的位置 · 第 2 周「Agent 执行」
D1-5 核心概念 D6 执行循环 D7 工作流 D8 提示词(给LLM的输入) D9 输出解析 D10 LLM抽象
L01

两级构建

🤔 每轮都要把 role/goal/工具/几十轮历史全拼进提示,循环几十轮后历史越来越长,不会撑爆 LLM 的上下文吗? 会。LLM 有硬性 token 上限(比如 8K/32K)。如果只会"把所有历史无脑拼进去",跑不了几轮就报"context length exceeded"。
💡 一句话本质:提示 = 固定的"系统提示" + 增长的"对话消息",超长时把旧历史压成摘要 系统提示(人设 + 工具 + 规则)相对固定,填一次模板;对话消息(历史 Feed)每轮增长,每轮重拼。历史超 token 预算时,把更早的旧历史用 LLM 压成一段"长期记忆摘要"(滚动记忆压缩,L06)——于是上下文永远塞得下:摘要(浓缩远期)+ 详细近期。这是 Day 06 迭代步里 build_agent_prompt + build_agent_messages 两步的展开。

提示词构建分两级:先填模板变量,再拼对话消息

  • ① 填模板AgentPromptBuilder):把 goals/instructions/工具塞进系统提示模板。
  • ② 拼消息AgentLlmMessageBuilder):系统提示 + 历史 Feed → LLM 的 messages 数组。
为什么分两级? 系统提示(定义智能体是谁、有什么工具、行为规则)相对固定,每轮基本一样——填一次模板。对话消息(历史往来)每轮增长——每轮重新拼。分开处理:模板管"人设与能力",消息管"对话上下文"。最终喂给 LLM 的是 [{role:system, 系统提示}, {role:user/assistant, 历史...}] 这个消息数组。

📋 生活类比(贯穿今天)系统提示像入职时发的员工手册——规定"你是谁、能用哪些工具、要守哪些规矩",发一次基本不变;对话消息每日工作日志——越写越厚,记着"我今天干了啥、结果如何"。每轮上工,都是"手册 + 到目前为止的日志"一起摆在 LLM 面前。
L02

系统提示模板

agent/prompts/superagi.txt 定义智能体人设,含占位符 {goals} {instructions} {constraints} {tools},并强制 LLM 只输出符合给定 JSON schema 的内容

# 模板要求 LLM 输出:
# thoughts: {text, reasoning, plan, criticism, speak}
# tool: {name, args}
为什么强制 JSON schema 输出? LLM 默认输出自由文本,但框架需要结构化地知道"它想调哪个工具、参数是什么"。系统提示里明确要求 LLM 按固定 JSON schema 输出(thoughts + tool),下一步 output_parser(Day 09)才能可靠地解析。这个 schema 和解析器是配套的——提示定格式,解析器按格式解。thoughts 里还分 text/reasoning/plan/criticism——引导 LLM 结构化思考(甚至自我批评 criticism),提升决策质量。
L03

填充主变量

AgentPromptBuilder.replace_main_variablesagent/agent_prompt_builder.py:65)把 goals/instructions/constraints 转成编号列表塞进模板。

读法:把"目标""指令""约束"渲染成 1. xxx / 2. yyy 的编号列表填进模板占位符。任务队列型工作流还会填 {current_task}/{pending_tasks}/{task_history}replace_task_based_variables:95),并按 token 预算截断历史。
📝 变量填充(输入 → 输出) 模板片段superagi.txt):GOALS:\n{goals}\n\nCONSTRAINTS:\n{constraints}
输入变量goals=["订最便宜的机票","预算不超过1000元"]constraints=["不要重复调用同一工具"]
→ replace_main_variables 渲染后
GOALS:
1. 订最便宜的机票
2. 预算不超过1000元

CONSTRAINTS:
1. 不要重复调用同一工具
每条独立编号,LLM 更容易逐条对照遵守。
为什么用编号列表? 给 LLM 的目标/约束如果是一大段文字,它容易忽略某条。渲染成编号列表(1. 2. 3.)让每条清晰、独立、易被 LLM 逐条对照。这是提示词工程的小技巧——结构化的输入让 LLM 更好地遵守。
生活类比:就像给人交代活儿,与其写一大段话,不如列一张清单(checklist):1. 做 A 2. 做 B 3. 别忘了 C。每条独立、可逐项对照打勾,不容易漏。
L04

工具渲染成 schema

add_tools_to_promptagent_prompt_builder.py:23)把每个工具渲染成 "工具名": 描述, args json schema: {...},并追加内置 finish 工具。

读法:每个工具的 name + description + args 的 JSON schema(Day 03 的 tool.args)拼进提示——告诉 LLM"你有这些工具、每个怎么用、参数怎么填"。_generate_tool_string:53)用 tool.args 的 JSON schema 说明每个工具的参数。总会追加一个 finish 工具——让 LLM 有办法"宣布任务完成"(Day 06 的结束条件)。
工具的动态构建(ToolBuilder,Day 12 预告) 这些工具实例是运行时动态构建的(ToolBuilder,Day 12)——根据 DB 里 Tool 记录的 folder/file/class 名 importlib 动态导入实例化,再注入依赖(llm/resource_manager…)。迭代步默认还会放一个 ThinkingTool(Day 03)。"哪些工具进提示"由智能体配置 + 工作流决定。
L05

拼装对话消息(含记忆)

AgentLlmMessageBuilder.build_agent_messagesagent/agent_message_builder.py:26):

# :38  system 消息 = 上面填好的系统提示
# :39-53  若 history_enabled,加当前时间,把历史 agent_feeds(Day 02 的 Feed)转成消息
# 超 token 预算的旧历史 → _split_history 切出去 → 压缩成摘要(L06)
提示词组装流水线(两级构建) 模板 .txt{goals}{tools}… 变量goals/工具/约束 系统提示system message 历史 FeedDay02 短期记忆 超 token 预算?_split_history LLM 压成摘要_build_ltm_summary 旧的→压缩 messages 数组[system, 摘要,近期历史…]
①模板+变量→系统提示;②系统提示+历史 Feed→messages;超预算的旧历史先被 LLM 压成摘要再拼入——上下文永远塞得下。
读法:最终 messages = system 提示 + 历史 Feed 转成的对话消息。这就是把 Day 02 的 Feed(短期记忆)读回来、拼进 LLM 上下文的地方——让 LLM"记得之前干了什么"。首次执行时还会把初始 messages 落库成 Feed,保证后续能回读。
🛠️ 如果让你自己拼消息(简化版 → 真实版对照) 朴素做法
prompt = system_prompt + "\n" + "\n".join(all_feeds)   # 全塞进去
问题:历史无限增长,循环几十轮后必然超 token 上限。
真实版build_agent_messages 把 system 与历史分开,且 _split_history超预算的旧历史切出去交 _build_ltm_summary 压成摘要(L06)。真实版多出来的"切 + 压"两步,就是为了解决朴素版"迟早爆上下文"的硬伤。
L06

滚动记忆压缩(关键机制)

历史越来越长会超 token 上限。_split_historyagent_message_builder.py:59)把超预算的旧历史切出去,交给 _build_ltm_summary:91调 LLM 压缩成"长期记忆摘要"作为一条 assistant 消息。

滚动摘要压缩解决什么? 智能体循环几十轮后,历史 Feed 可能几万 token,塞不进 LLM 上下文。做法:保留最近的详细历史,把更早的旧历史用 LLM 压缩成一段"摘要"("前面我做了 A、B、C,结论是 D")。于是上下文 = 摘要(浓缩的远期)+ 详细的近期。既不超限,又保留关键信息。这和 CrewAI/OpenHands 的历史压缩(CondensationEvent)完全同理——长任务智能体必备的"记忆管理"。"滚动"= 随着对话增长,不断把新变旧的部分卷进摘要。
📝 生活类比:就像长会议的会议纪要——早上讨论的细节没人全记,但会浓缩成一句"上午决定了 A、B、C";最近半小时的讨论还保留原话。摘要(浓缩远期)+ 原话(详细近期),既不啰嗦又不丢重点。
💥 不压缩会出什么事故?(数字具象) 假设每轮迭代往 Feed 里写 ~1500 token,循环 30 轮就是 ~4.5 万 token;而不少模型上下文上限只有 4K~16K token不压缩 → 第十几轮就报 context_length_exceeded,智能体直接崩在半路。压缩后:旧历史摘要 ~500 token + 最近 5 轮 ~7500 token ≈ 8K,稳稳塞下、还接着能跑几十轮。就为了这条命,滚动压缩必不可少。

👶 小白:把旧历史压成摘要,万一压掉了后面正好要用的关键信息,智能体不就"失忆"了?

👨‍🏫 老师:这确实是取舍,SuperAGI 尽量降低风险:只压更早的旧历史、完整保留最近几轮的详细记录;而且摘要是让 LLM 去提炼"做了 A/B/C、结论是 D"这类关键结论,不是粗暴截断。远期细节会丢一点,但这是拿"损失一点远期精度"换"上下文不爆、任务能跑完"——比第十几轮直接 context_length_exceeded 崩在半路强太多。

L07

模板与代码分离

注意提示词放在 .txt 文件里(prompts/superagi.txtagent_tool_input.txtagent_tool_output.txt),代码只做"读模板 + 变量替换"。

为什么提示词单独放文件? 智能体的提示词是需要反复调优的(改一个词可能大幅影响智能体表现)。如果提示词硬编码在 Python 字符串里,改它要动代码、要程序员。放 .txt 文件里——非程序员也能调提示、改了不用改代码、能单独 review 提示的变更。这是"模板方法 + 配置外置"的思想(和 CrewAI 把提示放 YAML、Day 19 一个道理)。Agent 开发的一大工作量是调提示,把提示外置是工程化的必然。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 提示词构建的两级(填模板 / 拼消息)?
  • 系统提示为什么强制 JSON schema 输出?
  • 工具怎么渲染进提示?为什么总追加 finish 工具?
  • 历史 Feed 怎么拼进消息?
  • 滚动记忆压缩解决什么?为什么提示词放 .txt?

✋ 动手

P=superagi/agent
cat prompts/superagi.txt | head -40
sed -n '23,93p' agent_prompt_builder.py | head -40    # 填变量 + 工具渲染
sed -n '26,70p' agent_message_builder.py              # 拼消息 + 切历史
grep -n '_build_ltm_summary' agent_message_builder.py
明天预告 · Day 09输出解析 output_parser——LLM 按 schema 输出后,怎么解析成"选哪个工具 + 参数"(AgentGPTAction)。两个 parser、JSON 容错、自我纠错。
← Day 07 工作流 Day 09 · 输出解析 →