跳到主要内容

3. 输入侧三块状态:提示 / 上下文 / 历史

这章讲发给模型的 messages 里“系统提示 + 历史”是怎么来的,以及如何在运行时把动态数据注入提示。

3.1 SystemPromptGenerator:分节模板

它不是让你写一大段 prompt,而是让你填三个列表:背景、步骤、输出指令。生成时拼成固定分节的 Markdown(context/system_prompt_generator.py:49-73):

# IDENTITY and PURPOSE
- <background 每条一行>
# INTERNAL ASSISTANT STEPS
- <steps 每条一行>
# OUTPUT INSTRUCTIONS
- <output_instructions 每条一行>
# EXTRA INFORMATION AND CONTEXT ← 仅当注册了 context provider 才出现
## <provider.title>
<provider.get_info() 的返回>

两个默认值得注意(context/system_prompt_generator.py:38-47):

  • 不给 background 时默认一句 "This is a conversation with a helpful and friendly AI assistant."
  • output_instructions 总会被追加两条硬指令:“始终用正确的 JSON schema 回”“善用额外上下文”。也就是说框架替你保证了结构化输出的提示纪律

3.2 ContextProvider:把运行时数据动态塞进提示

静态提示不够用时——比如“当前日期”“刚检索到的文档”——你注册一个 BaseDynamicContextProvider,它只需实现一个方法 get_info() -> str(context/system_prompt_generator.py:5-14)。

关键在时机:generate_prompt() 是在每次 _build_system_messages() 时被调的(回看 01-core-loop.md),而它会当场调每个 provider 的 get_info()。所以 provider 返回的是“此刻”的数据,提示是活的。

# 示意,非源码:一个把当前检索结果注入提示的 provider
class RagContext(BaseDynamicContextProvider):
def __init__(self):
super().__init__(title="Retrieved Documents")
self.chunks = []
def get_info(self) -> str:
return "\n".join(self.chunks) # 每轮生成提示时被现场调用,反映最新 chunks

注册/取用/注销在 agent 上有现成方法:register_context_provider / get_context_provider / unregister_context_provider(agents/atomic_agent.py:700-737)。它们本质是在操作 system_prompt_generator.context_providers 这个 dict。

3.3 ChatHistory:以 turn 为单位的多轮记忆

历史的最小单位不是“一条消息”,而是“一个 turn”。add_message 第一次会 initialize_turn() 生成一个 uuid,本轮所有消息(user 输入、assistant 回复、可能的工具结果)共享同一个 turn_id(context/chat_history.py:59-80)。

为什么要 turn?因为裁剪历史时要成对地删——不能删了用户问题却留着回答。01-core-loop.md 提到的 _trim_context 正是按 turn_id 整轮删除(配合 delete_turn_id,context/chat_history.py:207-231)。turn 是“保持对话语义完整”的删除单位。

溢出还有第二道闸:max_messages。超过就从头 pop(_manage_overflow,context/chat_history.py:82-88)——这是按条数的硬上限,和按 token 的裁剪是两套独立机制。

3.4 多模态:递归抽取图片/音频/PDF

历史里可以混进图片。get_history() 把每条消息序列化成发给模型的格式时,要把 Instructor 的多模态对象(Image/Audio/PDF)和普通文本字段分开:文本走 model_dump_json,多模态对象单独成项(context/chat_history.py:104-120)。

难点是多模态对象可能嵌在任意深度(列表里、字典里、嵌套模型里)。_extract_multimodal_info 用递归遍历整棵对象树,边收集多模态对象、边构造一个 Pydantic 兼容的 exclude 规格,告诉 model_dump_json “把这些字段排除掉”(context/chat_history.py:122-184)。

怎么读这段逻辑:递归到底,叶子若是多模态对象就标记排除并收集;往上汇总每层的 exclude。 还有个优化:若某个列表/字典的每一项都是纯多模态,就整字段排除(返回 True)而非逐项排除(context/chat_history.py:164-167)。

3.5 序列化:历史能存盘也能复活

dump() 把每条消息的 content 连同它的完整类名(module.ClassName)一起存成 JSON(context/chat_history.py:242-267)。load() 反过来:按类名字符串动态 __import__ 找回 schema 类,再 model_validate_json 重建对象(context/chat_history.py:269-314)。

这里有个多模态专属的复活步骤:Image/PDFsource 在 JSON 里是字符串,加载后要判断它是不是本地路径(不是 http/data: 开头),是的话转回 Path 对象(_process_multimodal_paths,context/chat_history.py:316-348)。Audio 行为略不同,代码注释明说只对 PDF/Image 处理。

3.6 关键细节 / 坑

  • provider 没注册时,提示里不会出现 EXTRA 节。 整段是条件渲染(context/system_prompt_generator.py:64)。
  • copy() 会连 current_turn_id 一起拷。 initial_history 就是靠 copy() 留底,reset_history 再从它复原(agents/atomic_agent.py:202212-216)。
  • load() 依赖 schema 类可被 import 到。 如果你的 schema 定义在临时作用域(比如函数内的局部类),_get_class_from_string 会找不到。

下一章:输出侧——工具、MCP 桥接、token。