跳到主要内容

别再死磕提示词优化,真正该优化的是上下文

· 阅读需 13 分钟

本文为 AgentScript 系列第二篇,承接上篇「模型到底看到了什么?」,提出一个核心认知:Agent 开发的核心优化方向,不是打磨提示词话术,而是管控模型可见的上下文信息

提示词优化,是提示词工程之后最顺理成章的进阶方向。 既然手写提示词脆弱、不稳定,那就交给优化器自动迭代:调整指令措辞、挑选示例、基于数据与指标编译出更优质的提示词。

这个思路本身合理,也诞生了不少实用工具。最典型的代表就是 DSPy,它将自身定位为面向大模型的编程框架,而非简单的提示词工具;内置优化器可以根据自定义指标,自动调优程序内的提示词甚至模型权重。

但对绝大多数智能体(Agent)而言,提示词优化并不是优先级最高的优化目标

问题不在于提示词不重要。 而是:提示词往往不该是最先优化的部分

一个 Agent 的完整提示词,从来不是单纯的一句指令,而是多部分混合而成:

完整提示词 = 系统指令 + 角色设定 + 上下文数据 + 示例 + 用户指令

常规的提示词优化,大多只聚焦指令相关部分:措辞润色、示例选择、任务话术调整、Few‑Shot 样例设计。 可实际上,Agent 的行为表现,更多由上下文数据决定:检索文档、工具返回结果、记忆记录、中间状态、历史输出、摘要引用、其他智能体的返回内容。

只要模型能看到正确的证据,哪怕指令平平无奇,也能给出不错的结果; 反之,如果模型获取的信息本身有误,再精妙的指令,只会让错误答案看起来更通顺、更像真的。

本文要讲的,就是另一个更关键的优化目标:

不要只优化你对模型说了什么,更要优化模型能看到什么

可选择的 context slot 让 context optimization 成为源码层面的决策

提示词,不是智能体的最小单元

单次大模型调用中,提示词似乎是天然的优化单元。 我们写指令、给示例、测试输出、微调话术,一套流程非常直观。

但这套思维,放到 Agent 多轮执行场景里就彻底失效了。 Agent 不止有一个固定提示词,它拥有不断变化的上下文边界

一个科研类智能体,上下文通常包含:

  • 用户原始问题
  • 检索关键词
  • 搜索结果
  • 召回文档
  • 文档摘要
  • 历史运行记忆
  • 中间观测信息
  • 失败尝试记录
  • 工具调用异常
  • 辅助智能体的输出
  • 最终答案格式约束

这些信息里,一部分需要传入下一轮模型调用,另一部分则必须过滤。

Agent 出问题,很少是因为:

指令描述不够清晰。

更多是因为这三类问题:

模型看到了错误的上下文。

正确的上下文存在于程序中,却没有传入提示词。

上下文传入了,但格式、粒度不对。

工具返回内容过长、记忆查询到过时信息、摘要丢失关键引用、规划步骤需要精简摘要而回答步骤需要完整原文…… 这些本质都不是话术问题,而是上下文选择问题

提示词文本:一个极难优化的软变量

提示词优化听起来很美好:用算法替代人工反复调试。 以 DSPy 的 MIPROv2 为例,它可以联合优化指令与 Few‑Shot 示例,基于下游任务指标,在无梯度、无模块标签的前提下,优化多阶段模型程序。 这是非常扎实且有价值的研究方向。

但它优化的对象,本质上是软性变量。 提示词文本存在这些天然缺陷:

  • 维度极高、自由文本无约束
  • 高度依赖特定模型特性
  • 约束困难、语义难以量化对比
  • 极易过拟合
  • 效果好时难以解释,效果差时难以排查

对比两次提示词的修改,只能看到文字变化,却很难解释系统为什么变得更稳定: 是指令本身真的更好? 还是某句话刚好契合当前模型的行为偏好? 还是示例刚好贴合验证集? 还是优化器找到了一个会随模型迭代失效的小技巧? 还是下游评估指标本身不完善?

这些问题不是否定提示词优化的价值,而是说明:自由文本形式的提示词,是一个非常难优化的目标。 它太贴近模型本身的黑盒行为,却远离了 Agent 内部真实的数据流问题。

上下文,才是更优质的优化目标

上下文和提示词完全不同。 上下文的核心不是一句话,而是一系列结构化决策

模型该看原始工具结果,还是精简摘要? 该看前3条文档,还是前10条? 该读取近期记忆、长期经验,还是完全不使用记忆? 该保留带引用的原文片段,还是压缩后的观测总结? 最终回答步骤需要完整证据,规划步骤是否只需要极简摘要?

这些都是清晰、可枚举的结构化选择:

  • 不使用上下文
  • 精简摘要
  • 常规总结
  • 高相关文档
  • 带引用的原文片段
  • 记忆+文档组合

同时它也更容易排查、追踪:

  • 本次运行用了前5条文档,token上限4k
  • 本次运行用了精简摘要,上限500token
  • 本次运行完全禁用记忆

上下文优化的结果,跨模型兼容性更强。 不同模型对话术敏感度差异巨大,但“是否使用召回文档”这个决策,几乎不受模型版本影响。GPT、Claude、Gemini、Qwen 等,无论风格差异多大,都依赖正确的信息输入。

上下文优化同样可能过拟合,在一类场景有效、另一类场景失效。 但它的优势在于:出问题时可观测、可定位。 你能清晰看到选了哪些信息、过滤了哪些信息、模型是否获取了关键证据。你修改的是结构化决策,而非模糊的话术。

从提示词工程,走向上下文工程

提示词工程关心的是:

我该对模型说什么?

上下文工程关心的是:

模型该看到什么?

这个转变至关重要。

在常规 Python / TypeScript 编写的 Agent 中,上下文通常以字符串、消息数组、框架对象、模板的形式存在。本地数据与提示词的边界,全靠开发者的编码习惯维持。

而 AgentScript 从一开始就定下更严格的规则:

本地数据,默认不是提示词上下文。

工具返回、记忆查询结果、中间观测、执行日志、其他智能体输出,不会自动混入提示词。 如果需要让模型读取某份数据,必须通过 use 显式声明。

use input.question as "用户问题"
use docs.summary max 4k as "参考证据"
use past max 2k as "过往经验"

这是第一步:让上下文变得可见、可控。 一条 use 语句直接定义:数据来源、分类标签、token上限,它不再是一段模板字符串,而是一份上下文契约

自然而然就走到了下一步: 既然上下文可以显式定义,那上下文的备选方案,同样可以显式定义。

use one of:把上下文搜索空间写进代码

AgentScript 用语法直接表达上下文的多种可选方案:

use one of {
none: empty
compact: scratch.digest max 500
verbose: scratch.summary max 4k
grounded: docs.top5 max 4k selected
} as "参考证据"

含义非常直白:

  • 存在一个名为「参考证据」的上下文模块
  • 可以选择完全不使用
  • 可以使用极简摘要
  • 可以使用常规总结
  • 可以使用带原文的高相关文档
  • 当前生效的方案是「带原文文档」

这里没有运行时魔法,没有隐藏的自动优化器,没有后台学习状态。 只是声明可选的上下文来源 + 标记当前最优选择use one of 定义搜索空间,selected 标记当前启用方案,empty 代表完全不启用。

模型不会看到备选列表,只会读取被选中的上下文内容。 如果选中原文文档,提示词就加载对应内容; 如果选中 none,该模块直接不出现。 全程可审计、运行时逻辑极简。

为什么“不启用”也要纳入统一机制

很多人会想:给可选上下文单独设计语法不就行了? 比如:

use? docs.top5 max 4k as "参考证据"

但这会让上下文可选性,变成另一套独立语法,增加语言复杂度。 更简洁的设计,是把「不启用」也作为一种备选方案:

use one of {
none: empty
docs: docs.top5 max 4k
} as "参考证据"

可读性更强,优化也更简单。 搜索空间一目了然:参考证据 =(不启用,文档)。 默认规则同样清晰:写在最前的方案为默认方案。 无需额外语法,上下文的有无,本身就是一种决策

selected:优化结果直接固化在源码中

这套设计最关键的一点:优化结果直接写在源代码里。 不会藏在数据库、运行时配置、后台优化器状态中,而是和其他备选方案写在一起。

use one of {
none: empty
compact: scratch.digest max 500
verbose: scratch.summary max 4k
grounded: docs.top5 max 4k selected
} as "参考证据"

程序自解释、可人工审查、可工具解析、可日志溯源。 版本控制的 diff 可以清晰展示变化:

use one of {
none: empty
compact: scratch.digest max 500
- verbose: scratch.summary max 4k selected
- grounded: docs.top5 max 4k
+ verbose: scratch.summary max 4k
+ grounded: docs.top5 max 4k selected
} as "参考证据"

这和传统提示词优化完全不同。 传统优化输出的是一段新话术; 上下文优化输出的,是一次明确的工程决策

优化模式:源码到源码的特化

AgentScript 最干净的优化模型,就是源码到源码的特化。 优化器不需要修改运行时、注入隐藏状态、改写提示词文本,只做一件简单的事:

  1. 读取 AgentScript 源码
  2. 找到所有 use one of 位置
  3. 评估每一种备选方案效果
  4. 移动 selected 标记
  5. 输出新的 AgentScript 源码

输入是 AgentScript,输出还是 AgentScript,运行时完全保持不变。

分两种可用形式:

  • 开发版:保留全部备选方案,方便后续迭代、审计、再优化
use one of {
none: empty
compact: scratch.digest max 500
verbose: scratch.summary max 4k
grounded: docs.top5 max 4k selected
} as "参考证据"
  • 生产版:直接固化为最优方案,精简代码
use docs.top5 max 4k as "参考证据"

如果最优方案是不启用,则直接删除该模块。

两种形式都通俗易懂,不需要复杂的运行时学习机制。

运行时应当保持简单纯粹

这是整个语言设计的核心约束:运行时不隐藏任何学习逻辑。 执行时,use one of 只会解析为其中一种确定方案,规则完全确定:

  • 标记了 selected → 启用该方案
  • 无标记 → 启用第一个方案

被选中的方案,行为和普通 use 完全一致。 选中原文文档,等价于直接写 use docs.top5...; 选中不启用,等价于没有这段上下文。

没有动态改写提示词、没有后台配置覆盖、没有自适应状态、没有运行时自修改。 日志完全真实可信:日志显示模型读取了什么,源码里就对应什么选择。

为什么比提示词优化更可审计

提示词优化的产物,本质还是一段文本:

你是严谨精准的助手,请基于上下文回答……

优化后话术再漂亮,也是一段难以审查的内容。 审阅者只能看到文字改动,却很难判断行为变化、跨模型稳定性。

而上下文优化的 diff,直白清晰:

- verbose: scratch.summary max 4k selected
+ grounded: docs.top5 max 4k selected

翻译成人话:模型现在读取原文文档,而非摘要

再比如:

- lessons: Lessons.relevant(input.question) max 1k selected
+ none: empty selected

含义:本轮不再给模型提供记忆信息

这些都是明确的工程决策:可审查、可追踪、可测试、可回滚。 讨论时不用纠结某句话术是否玄学,只需要讨论信息选择是否合理。

上下文优化同样需要评估

这套方案,不代表可以跳过评估环节。 优化器依旧需要评估信号:测试用例、验证输出、用户反馈、下游任务效果、人工评审。

上下文优化同样会过拟合:某套记忆策略在一类任务有效,另一类任务会造成信息污染;更多参考信息能提升真实性,但会增加延迟与成本。

核心区别不在于上下文优化绝对正确,而在于:优化变量全部可见。 出问题时,直接定位上下文选择;效果提升时,能清晰解释背后原因。

Agent 不是单次模型调用,而是完整程序。程序需要可审查的状态、明确的边界、可理解的变更,这正是上下文优化的优势。

对 AgentScript 的意义

AgentScript 的第一个核心理念:

智能体上下文,应当由代码定义。 这由 use 实现。

第二个核心理念:

上下文的全部可选空间,也应当由代码定义。 这由 use one of 实现。

这是从上下文工程,走向上下文优化的桥梁。 语言本身不需要做自适应话术、优化角色描述、动态修改控制流,只需要维持极简的核心模型:

  • use:声明模型可见信息
  • generate:声明模型调用位置与输出约束
  • one of:声明上下文备选方案
  • selected:标记当前最优选择
  • empty:声明刻意不启用某类上下文

靠这几个基础原语,就能实现可审计、可迭代的上下文优化,同时不把复杂逻辑藏进运行时。

真正的问题

传统提示词工程问:

我该对模型说什么?

AgentScript 最初解决的问题:

模型到底看到了什么?

上下文优化新增的问题:

模型本可以看到哪些信息,我们为什么选择这一种?

这就是提示词优化和上下文优化的本质区别: 提示词优化,是在语言层面搜索; 上下文优化,是在证据、记忆、摘要、工具结果、token上限、信息取舍层面搜索。

对智能体而言,后者往往重要得多。 不是提示词不重要,而是在真实 Agent 程序中,模型输出的上限,很少由话术优美度决定,更多由输入信息的质量决定。

可靠的智能体,不止需要更好的提示词,更需要显式、有作用域、可审计、可优化的上下文选择。 这正是 AgentScript 正在探索的方向:

上下文即代码,上下文可选空间即代码,优化结果即代码。

模型到底看到了什么?

· 阅读需 10 分钟

本文介绍 AgentScript,一门用于构建显式、有作用域、可审计LLM 上下文的小型领域专用语言。

我们这一代很多程序员,最初接触编程时都遵循一套简单模型:输入、处理、输出

程序接收数据、处理数据、产出结果。这套模型虽然古老,但十分好用,它让程序的边界清晰可见,逻辑易于理解。

而 LLM 智能体(Agent)彻底打破了这套传统范式。

智能体的输入不再只是文件、接口请求、结构化记录,而是复杂的提示词上下文:用户意图、工具返回结果、检索文档、记忆记录、中间状态、重试日志、其他智能体的输出……

输出也不再是简单的返回值,而是一段生成文本或 JSON,必须满足约定格式,才能被后续流程信任与复用。

绝大多数 Agent 项目失败,不是因为调用大模型太难,而是没人能确切知道:模型生成结果前,到底看到了哪些信息。

经过多轮迭代后,Agent 会积累大量局部变量、工具返回、记忆数据、中间观测结果、重试消息、其他智能体输出。一部分数据需要传入下一次模型调用,另一部分则应当隔离。 在绝大多数 Python、TypeScript 实现的 Agent 中,上下文边界全靠开发者的编码习惯维护。

这套方式在小型 Demo 中尚可,但放到真实业务流程里,会变得极度脆弱。

传统追加式 chat 与 AgentScript scoped context boundaries 的对比

模型到底看到了什么? 哪条工具结果被加入提示词,哪些只是本地临时数据? 记忆是否被截断? 其他智能体的输出,是作为参考证据,还是混入了历史对话? 模型必须输出什么格式,才能进入下一步流转?

AgentScript 的诞生,就是为了让这些问题直接能从代码里找到答案

AgentScript 是什么

AgentScript 是一门面向 LLM 智能体的小型编程语言,核心目标是让提示词上下文具备显式定义、作用域隔离、类型约束、可追踪、可审计的能力。

它主要面向开发者构建多步骤智能体场景,严格管控工具输出、记忆、中间状态与生成内容。

不是提示词模板,不是 YAML 配置,也不是通用 Agent 框架。

它的核心理念一句话概括:

智能体上下文,应当由代码显式定义。

语言最核心的两个关键字:usegenerate

use content max 8k as "文件内容"

generate({
input: "为忙碌的同事总结这份文件",
max_output: 1000
}) -> {
title
summary
key_points: list[string]
action_items: list[string]
}

use 声明哪些数据可以被模型看到generate 定义调用大模型的唯一入口,同时约束输出结构。

在传统输入-处理-输出视角下,AgentScript 直接聚焦 LLM 程序最不稳定的两个边界:提示词输入、生成结果输出

这门语言真正的价值,不是封装了大模型调用,而是把提示词边界直接暴露在代码层面,一目了然。 语言其余能力:变量、函数、智能体、导入、循环、工具、记忆、执行追踪,全部都是为了支撑这套上下文管控逻辑。

为什么不直接用 Python / TypeScript?

Python、TypeScript 都是优秀的通用编程语言,AgentScript 并非要替代它们。

问题在于:通用语言没有原生的“提示词上下文”概念。 上下文往往以字符串、数组、对象、模板、框架调用、消息列表的形式零散存在。代码逻辑没问题,但上下文意图散落在各处:

const messages = [
system("你是一名审阅者"),
user(`问题:${input.question}`),
user(`搜索结果:${JSON.stringify(results)}`),
user(`历史记忆:${memory.map((item) => item.text).join("\n")}`),
];

const answer = await model.generate(messages);

results 里哪些字段被传入?是否混入了原始工具输出?记忆是否被截断?其他智能体输出是否被作为历史对话?返回结果需要满足什么格式? 这些问题,在通用语言里完全无法显性约束

而 AgentScript 将上下文选择作为一等公民操作:

use input.question as "用户问题"
use results.summary max 4k as "搜索结果"
use past max 2k as "过往经验"

generate({
input: "仅使用选中的上下文回答",
max_output: 800,
strict: true
}) -> {
answer
citations: list[string]
}

局部变量、工具结果、记忆查询结果、执行追踪,不会自动进入提示词。 只要数据需要被模型看见,就必须通过 use 显式声明。

就这一条规则,彻底改变了智能体的开发方式:提示词不再是字符串拼接的副作用,而是一套有作用域、可契约化的约束。

极简示例:本地文件总结智能体

下面是一个完整的文件总结 AgentScript 程序:

import llm Qwen from "ollama://localhost:11434/qwen3.6"
import tool File from "file://workspace"

main agent FileSummarizer {
model Qwen
role "技术文档撰写人"
description "读取本地文件,生成结构化摘要。"

main func(input { path: string }) {
content = File.read({
path: input.path
})

use input.path as "文件路径"
use content max 8k as "文件内容"

generate({
input: "为忙碌的同事总结这份文件",
max_output: 1000
}) -> {
title
summary
key_points: list[string]
action_items: list[string]
}
}
}

文件工具可以读取工作目录,但工具返回结果不会自动成为提示词上下文。程序手动选择路径与文件内容、打上标签、设置上下文长度上限,再要求模型输出结构化结果。

直接运行:

agentscript run recipes/summarize-file.as --input '{"path":"README.md"}'

使用模拟模型快速验证:

npm install -g @rong/agentscript
agentscript run recipes/summarize-file.as --input '{"path":"README.md"}' --mock --trace

支持多种运行模式:

  • --mock:使用确定性模拟模型
  • --dry-run:仅构建提示词,不调用模型
  • --trace:输出完整审计日志

执行日志可以清晰展示:选中了哪些上下文、设置了哪些长度限制、哪些内容被截断、使用了什么指令、输出格式要求、校验是否通过。 日志用于调试与审计,本身不会被传入模型

典型日志示例:

Generate #1
Agent: FileSummarizer / 技术文档撰写人
选中上下文:
[文件路径] input.path
[文件内容] content, 上限=8k, 未截断
指令:
为忙碌的同事总结这份文件
输出结构:
title 字符串
summary 字符串
key_points 字符串列表
action_items 字符串列表
校验: 通过

generate:唯一的模型调用入口

在 AgentScript 中,普通代码可以计算数据、调用工具、查询记忆、调用其他智能体、维护中间状态。只有 generate 能发起模型调用

answer = generate({
input: "仅使用选中的上下文回答",
max_output: 800,
attempts: 3,
strict: true
}) -> {
ok: boolean
answer
citations: list[string]
}

-> 后定义输出契约:AgentScript 会强制校验模型返回格式,JSON 异常或结构不匹配时自动重试。下游代码可直接依赖结构化输出,无需手动解析文本。

每一次模型调用都有清晰边界:

  • 当前所属智能体身份
  • 通过 use 选中的上下文
  • generate 内的指令
  • 可选的输出结构约束

这个边界,就是调试、审计、复盘的最小单元。

作用域 = 上下文边界

AgentScript 依靠代码作用域管控上下文可见性。 use 声明仅对当前作用域、子作用域内后续 generate 生效,不会向上泄漏。函数、智能体调用会创建独立上下文边界。

func caller(input) {
use input.goal as goal
helper(input)
}

func helper(input) {
use input.detail as detail

generate({ input: "基于细节处理" }) -> {
ok: boolean
}
}

helper 内部的模型调用只能看到 input.detail,不会自动继承上层 goal。 跨智能体调用同理:被调用智能体只能看到传入参数与自身 use 声明,不会继承调用方的提示词上下文。 多智能体组合因此更易审计,每个智能体拥有独立上下文契约,而非共享混乱的全局对话缓冲区。

工具返回是数据,不是上下文

AgentScript 最重要的规则之一:工具返回结果是本地程序数据,不会自动进入提示词,必须显式 use 选中

在代码评审、科研检索、代码分析等场景,工具往往返回大量冗余数据,远超出模型需要的范围。 例如仓库评审场景,可以先收集文件树、TODO 标记、包信息、CI 配置,再只选取必要部分:

use "文件树" 上限=8k
use "TODO发现" 上限=4k
use "包元信息" 上限=4k
use "CI配置" 上限=4k
generate 输出阻碍点、风险、可快速优化项、下一步计划

工具拓展程序能力,use 管控模型可见范围,二者严格区分。

记忆同样显式可控

AgentScript 内置 JSONL、SQLite 两种记忆存储后端,但记忆遵循和工具完全一致的规则: 记忆句柄只是能力,不会自动混入上下文

import memory Lessons from "file://./.agentscript/lessons.jsonl"

智能体需要先查询记忆,拿到普通数据,再通过 use 手动纳入上下文:

past = Lessons.query({
text: input.goal,
kind: "lesson",
limit: 5
})

use input.goal as goal
use past max 2k as "过往经验"

写入记忆同样显式可控:

Lessons.add({
kind: "lesson",
text: reflection.insight,
goal: input.goal
})

支持反思、自我迭代,同时避免上下文无限膨胀。后续运行可复用历史经验,但全程可追溯、可管控。

智能体模式:基于基础原语自由组合

AgentScript 不会硬编码 Planner、Executor、反射等特定模式关键字。 ReAct、规划-执行、评估-优化、反思、自我迭代、多智能体协作,全部由一套极简原语组合实现:

  • 智能体、函数:划分边界
  • 工具:扩展外部能力
  • 记忆:持久化状态
  • use:管控提示词上下文
  • generate:模型调用与输出契约
  • 执行追踪:可审计

针对并行独立任务,内置 parallel for

results = parallel for step in plan.steps max 10 {
Executor({
goal: input.goal,
step: step
})
}

并行执行结果仍是本地数据,只有手动 use 后才会进入后续提示词

use results.summary max 6k as 执行结果

项目现状

AgentScript 仍处于实验阶段,但核心语言设计已稳定落地。

已实现功能:

  • 语法解析器、语义校验器
  • 模拟运行时
  • OpenAI / Anthropic / Ollama 模型适配器
  • 文件、环境变量、HTTP、Shell 工具
  • JSONL / SQLite 记忆后端
  • 结构化输出校验
  • 执行追踪
  • 基础运算符、赋值、并行循环
  • 完整 CLI 命令

可用于实验、示例、本地工作流,1.0 正式版前语法可能微调。

后续规划:稳定中间表示、完善错误提示、VS Code 语法高亮支持。

项目现阶段目标不是做成熟生产级框架,而是验证一个核心理念:

智能体程序最重要的部分,或许不是模型调用的框架封装,而是调用前的上下文契约

快速上手

安装 CLI:

npm install -g @rong/agentscript

运行示例:

agentscript run recipes/summarize-file.as --input '{"path":"README.md"}'

免安装直接运行:

npx @rong/agentscript run recipes/code-review.as --input '{"path":"src"}'

模拟运行、调试追踪、离线校验,一键即可。

项目地址:

结语

行业里谈论智能体,总离不开工具、记忆、规划、自主性。 但所有能力,都依赖一个最基础的问题: 模型生成下一段输出前,到底看到了什么?

AgentScript 正是围绕这个问题构建。它将提示词上下文变成可声明、可限定、可管控、可校验、可追踪的代码契约。

我们相信:可靠的生产级智能体,需要上下文工程成为编程范式,而非靠编码习惯约束