写给AI的上下文

从写 Prompt,到设计 Agent 的信息环境

目标和面临的问题

  • 我想要智能体按照预期的方式进行工作
  • 因此,我要给智能体写上下文信息,让它帮我按预期的方式工作
  • 面临的问题:
    1. 哪些内容要写进上下文,哪些不要写进上下文
    2. 上下文信息膨胀,越来越难以维护
    3. 智能体不按我预期设定的步骤工作
    4. 智能体执行不稳定, 时好时坏
    5. 信息太多, 相互冲突
    6. Skill 越来越多,人不知道该调用哪个
    7. ...

上下文指针

定义

上下文指针,是当前上下文中指向外部知识或规则的一条延迟加载入口。

它告诉 Agent:

什么情况下,需要去哪里获取什么信息,以及获取这些信息后应该如何使用。

一个有效的上下文指针至少包含三个信息

触发条件
    ↓
去哪里
    ↓
拿到之后做什么

例如:

任何 API 的创建、修改或删除操作,
必须先阅读 docs/api.md,并遵循其中的 API 设计规范。

拆开:

部分 内容
Trigger API 的创建、修改或删除
Target docs/api.md
Action 阅读并遵循其中规范

弱指针

API 设计请参考 docs/api.md

问题:

什么情况下参考?

Agent 可能理解成:

API 设计
    ↓
“哦,这里有个文档”

但是并没有明确:

  • 创建 API 要不要看?
  • 修改 API 要不要看?
  • 删除 API 要不要看?
  • 是开始设计之前看,还是遇到问题再看?
  • 看完之后必须遵守吗?

强指针

任何 API 的创建、修改或删除操作,
必须先阅读 docs/api.md,并遵循其中的 API 设计规范。

这里实际上形成了:

API 创建 / 修改 / 删除
          ↓
      读取 api.md
          ↓
      遵循其中规范

总结:好的上下文指针,不只是告诉 Agent“资料在哪里”,而是告诉 Agent“什么时候应该进入这份资料”。

上下文负载和认知负载

上下文负载

指的是写进上下文的每个词都会持续不断消耗 token,而且稀释模型的注意力,所以要对上下文的长度进行必要的控制。

认知负载

指的是我们写的上下文,以及上下文指针所指向的文件,人都要去理解和记住,知道上下文到底干了什么关于这些需要记住的东西,以及人需要决策的东西,构成了人的认知负载 比如你的项目有:

50 个 Skill 30 个 Rule 100 个文档 20 个 Command

Agent 可能能找到,你可能不记得哪个起什么作用,或者系统中都有哪些 skill 规则等等。 所以这就要求我们的上下文组织是有结构、有体系的,容易导航的。

好的设计和不好的设计

在不需要人工判断的地方,就把认知负载去掉。 例如:

不好的设计

每次开发者都要记:

API 修改 → 运行测试

数据库修改 → 更新 migration

Go 代码修改 → gofmt

提交代码 → golangci-lint

这会消耗人的记忆。

可以交给 Agent:

Agent ↓ 自动执行

于是:

机械判断 ↓ 自动化

而:

真正的工程判断 ↓ 留给人

           成本应该由谁承担?
                    │
          ┌─────────┴─────────┐
          ↓                   ↓
   机器可以机械完成        必须人做判断
          ↓                   ↓
     Context/Agent         Human

例子

设计一个自动化部署 Skill:

  1. 是否提交工作区的修改:由人判断。
  2. 确定部署后:由 Agent 自动完成打包、构建、推送、更新服务、部署验证等流程。

也就是:

工程判断 → 人
机械执行 → Agent

把不需要人工判断的流程交给 Agent,可以减少人的认知负载。

信息层级

上下文内容的类型

一个文档中的内容可以分成两类:

  1. 步骤:智能体需要执行的动作,即“怎么做”。
  2. 参考信息:智能体需要时查阅的定义、规则和事实,即“是什么、应该遵守什么”。

上下文信息的三个层级

        Agent 当前任务
              │
              ▼
       ┌─────────────┐
Tier 1 │ 直接执行     │  ← Steps
       └─────────────┘
              ↓
       ┌─────────────┐
Tier 2 │ 当前文件参考 │  ← Reference
       └─────────────┘
              ↓
       ┌─────────────┐
Tier 3 │ 外部按需获取 │  ← Pointer → Reference
       └─────────────┘

Tier 1:直接执行

当前任务需要执行的步骤,直接放在当前上下文中。

Steps

1. 找到 API
2. 检查 API 命名
3. 修改 API

Tier 2:当前文件参考

当前文件中包含一些参考信息,Agent 在执行步骤时按需参考。

Steps

1. 找到 API
2. 检查 API 命名
3. 修改 API

Reference

### API 命名规则
- URL 使用复数名词
- GET 用于查询
- POST 用于创建

例如当前任务涉及 API 命名:

Steps
  ↓
涉及 API 命名
  ↓
参考当前文件中的 API 命名规则

Tier 3:外部按需获取

如果参考信息比较多,或者只在部分任务中需要,可以通过上下文指针指向外部文档。

Steps
  ↓
涉及 API 命名
  ↓
读取指针
  ↓
api-rules.md
  ↓
获取 API 命名规则

例如:

For API naming rules, see ./api-rules.md

如何决定信息应该放在哪一层?

最简单的方法是做分支测试

              一块信息
                  │
                  ▼
          所有分支都需要吗?
             /          \
           是            否
           ↓              ↓
        Inline          Pointer
           │              │
       当前文件        外部 Reference
           │              │
           ↓              ↓
       直接可见          按需获取

如果所有任务都需要这块信息,就直接放在当前上下文。

如果只有部分任务需要,就考虑下沉到外部文档,通过上下文指针按需获取。

所以:

总结:信息层级的核心不是把文档拆得越细越好,而是让信息出现在它真正需要出现的地方。

信息共置

逻辑上属于同一个概念的信息,应尽可能放在相近的位置,而不是分散在文档各处。

例如 API 错误处理:

┌─────────────────────┐
│ API 错误处理         │
│                     │
│ 定义                 │
│ 规则                 │
│ 例外                 │
│ 示例                 │
└─────────────────────┘

而不是:

定义 ────────────────┐
规则 ────────┐       │
例外 ────────┼───────┘
示例 ────────┘

这样 Agent 阅读一个概念时,可以一次获得它相关的定义、规则和例外。

总结:信息层级决定“放多深”,信息共置决定“放在一起”。

Information Hierarchy
→ 信息应该放多深

Progressive Disclosure
→ 不那么即时的信息逐步下沉

Context Pointer
→ 告诉 Agent 去哪里找

信息共置(Co-location)
→ 相关信息放在一起

完成标准

每一个步骤都应该有明确的完成标准,用来告诉 Agent:

什么情况下,这一步才算完成?

完成标准有两个关键属性:

1. Clarity:完成边界清晰

Agent 能明确判断“已完成”和“未完成”。

❌ 模糊:

Step:
分析代码

Done:
理解代码

Agent 很容易:

看完代码
  ↓
觉得自己理解了
  ↓
Done

造成提前完成

✅ 清晰:

Step:
分析 Bug

Done:
- 找出触发入口
- 找出实际调用链
- 明确 Bug 根因
- 在 analysis.md 中记录结论

完成边界越清晰,越不容易提前结束。

2. Demand:完成要求充分

Clarity 解决:

什么算完成?

Demand 解决:

需要做到什么程度?

例如:

检查所有受影响的 API

这里的“所有”就是覆盖要求。

它不需要把每一次搜索、核对都写成 Step,而是要求 Agent 自己完成必要的调查。

所以:

Clarity
→ 划清完成边界

Demand
→ 决定完成程度

例子

❌ 模糊

理解登录模块

✅ 清晰

完成条件:

  1. 修改 login.go
  2. 添加失败场景测试
  3. go test ./... 通过
  4. 返回值符合 API 规范

Leading Word(先导词):用工程概念撬动 Agent

什么是 Leading Word?

Leading Word 是一个能够唤起一整套已有概念和行为的词。

它的价值不只是“给一个概念起名字”,而是:

一个词
  ↓
激活模型已有的概念关联
  ↓
带出一整套知识、方法和行为

例如:

Tracer Bullet

不只是一个词。

它背后关联着:

最小端到端路径
      ↓
尽早验证整体方向
      ↓
再逐步增加复杂度

所以,与其重新解释一遍,不如直接使用成熟的工程概念。


为什么优先使用成熟概念?

因为模型的预训练已经积累了大量成熟概念。

成熟术语
   ↓
已有 Prior
   ↓
激活已有知识
   ↓
用较少的 Context 表达较大的概念

而自己创造一个词:

自造词
  ↓
没有现成 Prior
  ↓
必须先定义
  ↓
再建立这个词与概念的关联

所以:

优先使用已有的成熟概念,而不是轻易创造新的术语。

这不是单纯为了节省 Token,而是为了借用模型已经具备的概念结构


工程概念正在成为控制 Agent 的语言

过去:

工程师
  ↓
工程思想
  ↓
代码

现在:

工程师
  ↓
工程思想
  ↓
Spec / Skill / Context
  ↓
Agent
  ↓
代码

Agent 加入了软件生产过程。

因此,工程师需要的不只是“会写 Prompt”,而是拥有足够丰富的工程概念

TDD
Tracer Bullet
Code Review
Refactoring
Definition of Done
Context Boundary
Progressive Disclosure

这些概念正在从:

人类工程师之间交流的方法论

变成:

人类用来定义、约束和纠偏 Agent 行为的语言。


一个更深的问题

如果一个工程师只有:

“帮我把代码写好”

那么他实际上缺少能够控制 Agent 的“旋钮”。

而拥有更多工程概念之后,他可以进一步表达:

怎么验证?     → TDD
做到什么程度? → Definition of Done
如何逐步实现? → Tracer Bullet
如何检查?     → Code Review
如何控制上下文?→ Context Boundary

所以:

工程知识越丰富,能够施加给 Agent 的约束和判断维度就越丰富。

这也是 AI 时代学习软件工程的一个重要原因。

核心思想

Leading Word 不只是让 Prompt 更短,而是让一个成熟的工程概念成为控制 Agent 的入口。

更进一步:

软件工程知识,正在从“写好软件的方法”,变成“定义和控制 Agent 如何生产软件的语言”。

总结

Leading Word
     │
     ↓
① 概念压缩
一个词 → 一大片已有知识
     │
     ↓
② 行为锚定
让 Agent 更稳定地进入某种行为模式
     │
     ↓
③ 调用锚定
让 Pointer 更容易指向正确的上下文
     │
     ↓
④ 共享语言
Prompt / Skill / Docs / Code
使用同一套工程概念

正向表达:不要激活错误行为

否定也是一种激活

Agent 的上下文不仅是在约束行为,也在激活概念

因此:

❌ 不要使用全局变量

虽然是在禁止,但同时把:

全局变量

这个概念带进了上下文。

可以理解为:

Don't do X
    ↓
X 被激活
    ↓
否定只是对 X 的修饰

所以,否定式指令可能不是最好的行为引导方式。


优先描述目标行为

与其反复告诉 Agent:

不要做 X
不要做 Y
不要做 Z

不如直接告诉它:

应该做 A
应该做 B
应该做 C

例如:

❌ 不要写过长的注释

改成:

✅ 使用简洁的一行注释

核心区别是:

负向表达
    ↓
激活禁止行为

正向表达
    ↓
激活目标行为

什么时候可以使用否定?

否定并不是绝对不能使用。

当某个条件属于必须禁止的硬性边界时,否定仍然有价值:

只操作测试数据库。
禁止修改生产数据库。

更好的方式是:

正向目标
   ↓
应该在哪里做

+

硬性护栏
   ↓
绝对不能越过什么边界

也就是:

优先用正向表达引导行为,必要时再用否定建立硬性护栏。

核心思想

上下文不是单纯的规则清单,它同时在激活 Agent 的注意力和行为。

因此,写上下文时要问:

我写下这句话时,究竟是在激活我希望 Agent 做的事情,还是无意中激活了我不希望它做的事情?

No-op:没有行为增量的内容

什么是 No-op?

No-op 来自程序设计中的 No Operation

写进上下文,但并没有改变 Agent 的行为。

因此可以逐句检查:

一句话
  ↓
Agent 原本就会这样做吗?
  ↓
是 → No-op → 删除
否 → 保留

例如:

Be helpful.

如果模型本来就会尽量提供帮助,这句话就没有产生新的行为约束。


关键:看 Agent 的行为,而不是人的感觉

判断 No-op 是 model-relative 的。

不是问:

“这句话有没有意义?”

而是问:

“加上这句话以后,Agent 的行为有没有发生变化?”

可以通过简单的 A/B 实验验证:

版本 A:Review the code.

版本 B:Review the code.
        Be thorough.

        ↓

比较两次执行结果
        ↓
行为没有明显变化
        ↓
Be thorough → 可能是 No-op

所以不要只凭感觉判断,运行文档本身就是验证方法


No-op 就直接删除

如果一句话被验证为 No-op,不要只是继续润色:

❌ Be very helpful and reasonably thorough.

        ↓

“帮它改得更漂亮”
        ↓

❌ Be helpful and thorough.

如果没有行为增量,就应该:

直接删除

因为:

给 No-op 换一种说法,仍然可能是 No-op。


Leading Word 也需要通过 No-op 检验

Leading Word 并不是用了就有效。

例如:

thorough

如果模型默认已经足够 thorough,那么:

Be thorough.

可能没有增量。

这时可以:

No-op
  ↓
换一个行为区分度更高的概念
  ↓
实验验证

重点不是某个词一定更好,而是:

Leading Word 必须真正产生行为上的增量。


核心思想

Pruning 不只是删除重复和过期内容,还要检查:

这一句话到底有没有改变 Agent 的行为?

因此:

重复 meaning     → 删除
环境已有信息     → 删除
无关内容         → 删除
过期内容         → 删除
No-op            → 删除

最终目标:

让上下文中的每一行,都尽可能产生实际的行为价值。

最后总结

前面讲的这些原则,其实已经被 writing-for-agents 这样的 Skill 总结和固化了。

所以实践时:

writing-for-agents
        ↓
直接使用
        ↓
再用前面的原则检查结果

这些内容,是为了知其所以然;真正写的时候,用 Skill 就够了。

评论 (0)

暂无评论,来抢沙发吧~

登录 后发表评论