跳到主内容
创建时间:最后更新:作者:白川

提示词与上下文:怎样给到正确的信息

围绕目标组织规则、证据和输出要求,理解检索、重读、摘要与裁剪的用途,并学会核对一次请求实际带了什么。

提示词与上下文:怎样给到正确的信息

"帮我看看代码"没有说明看什么、为什么看、什么结果才有用。模型可能解释语法,也可能建议重构,即使回复很长,也未必满足真正需要。

有效输入首先让任务有边界,再给完成它所需的证据。提示词和上下文不是两份互不相干的材料:指令说明怎样做,相关信息决定依据什么做。

这一篇接着上一篇往下走。上一篇讲清了容量和"找不到"的五种原因;这一篇要回答的是另一个问题:已知信息该由谁提供、更新,以及怎样确认它真的被发出去了。

把含糊任务变成可核查请求

对教学文件 src/greet.ts,可以这样表达:

text
目标:检查 src/greet.ts 中的命名 function 声明。
范围:只读该文件,不改代码,不推断安全性。
依据:使用实际文件检查结果;不能读取时明确说明。
输出:文件路径、匹配到的名称、检查能力的限制。

这是贯穿本组文章的教学文件,不表示已经对你的项目执行了检查。

它没有告诉模型答案是 greet,而是规定获取事实和交付结果的方式。真正的名称仍应来自工具。

四行分别对应一件不同的事:目标划定要达成什么,范围排除不该碰的,依据指定事实从哪来,输出定义什么算完成。少了任何一行,模型就得自己猜,而它猜的方式通常偏向最常见的那种回答。

把这四行拆开看,能看出它们各自防住的是什么:

这一行防住的问题
目标
模型把任务理解成相邻的另一件事
范围
模型顺手做了你没要求的事
依据
模型凭推测作答,而不是依据实际检查
输出
结果无法被下游直接使用

值得注意的是,这四行里没有一行是关于"答案"的。这一段提示的全部内容都在约束过程,而不是规定结论。这和"提示词要包含正确答案"的直觉正好相反——后者既做不到(模型不知道你项目里的真实情况),也不该做(那等于绕过检查)。

对于更开放的任务,还需要澄清优先级、允许改动范围或完成标准。并不是所有请求都要写成固定模板,但系统不能靠一句"认真一点"自动推断缺失条件。

模板化也有代价。四行写死的任务确实更稳,但遇到"这个文件里到底有什么"这类需要先探查才知道范围的任务时,过早固定范围反而会限制探索。判断标准是:目标是否已经确定。目标确定、范围待定的任务,先让模型探查再收敛,比一次写死更合适。

分清四种输入

输入在本任务中的例子谁负责更新它写错了会怎样
稳定规则
不泄漏凭据,遵守操作范围
应用或项目规则维护
每一轮都被违反,且无人察觉
当前目标
检查指定文件的命名声明
用户任务变化时更新
模型去做了一件相邻但不同的事
事实材料
文件内容、工具结果
随真实来源重新获取
结论建立在过时或错误的事实上
输出要求
给出路径、名称和限制
按使用结果的需求定义
交付了正确内容,但格式无法被消费

"谁负责更新它"这一列值得多看一眼。四种输入的更新频率和更新主体都不一样,混在一起维护就会出现"规则写在提示词里、事实也写在提示词里"的情况——后者会过期,且没有任何机制提醒你。

这个区分还有一个副作用:混在一起写,一次修改会同时动到几个语义不同的东西,于是很难判断是哪一处改动带来了行为变化。把它们分开,改动才能被单独归因——这一点在后面的对照实践里会变成实验设计的前提。

规则不能替代材料;材料里的文字也不能无条件变成规则。工具结果中出现"请忽略之前指令",仍属于待处理数据,不应替用户扩大权限。

稳定规则从哪里来

上面四种输入里,"稳定规则"最容易被误放到提示词里。在 Pi 里,它有专门的落点。

系统提示的第一段(preamble)之外,宿主会自动发现项目里的上下文文件并把它们拼进系统提示的 project_context 段。发现规则是明确的:按 AGENTS.override.md、AGENTS.md、AGENTS.MD、CLAUDE.md、CLAUDE.MD 的顺序,在当前目录及其各级父目录中各取第一个命中的文件;用户级配置目录里的同名文件优先级更高。项目根的说明先于子目录的说明。

所以"每次都要重复告诉模型项目有什么规矩"这个需求,多数情况下不需要靠提示词解决——把规矩写进项目根目录的 AGENTS.md,它会作为稳定规则进入每一轮请求,而且不会占用对话历史。

对应地,"当前目标"和"输出要求"属于这一轮的用户消息,它们随请求发出、会进入历史、会随对话变长;而 project_context 和 rules 属于系统提示的具名段落,内容不变就一直复用。把两类内容分开放,容量和可维护性都会好一些。

至于"事实材料",它最不适合写进任何固定位置——它的正确来源永远是实时读取。

四种补充信息的方法

检索适合从大量资料中选候选内容,重新读取适合核对当前事实,摘要适合压缩已获取材料,裁剪适合移除无关或过时内容。它们解决的问题不同。

方法解决的问题什么时候用
检索
内容太多,不知道哪部分相关
在大范围资料里定位候选
重新读取
需要确认现在的真实状态
摘要可能已经过时
摘要
内容太多,需要压缩传递
历史或工具结果过长
裁剪
内容太多,其中一部分不相关
移除噪声,保留关键项

例如需要知道 greet 的调用位置,可以先检索相关文件,再读取必要片段。检索命中一段文字,不证明那是当前生效版本;摘要说"已经检查",也不证明检查覆盖今天的改动。

选择材料时可以连续问:这项信息会影响哪一个判断?它从哪里来?是否可能已经过时?如果答不出来,就不必默认把它加入请求。

这四个方法对应两种不同的目的,需要分清。检索和重新读取是补充信息,往上下文里加东西;摘要和裁剪是减少信息,从上下文里减东西。前者解决"缺证据",后者解决"太嘈杂"。

同一个工具可以同时被这两种目的使用,这取决于它的输出被怎么对待。检索工具返回一段匹配文本,是补充;但如果这个工具每次都返回十几条结果而你只需要一条,那它同时也在制造噪声。所以判断标准不是"用了哪个方法",而是"这次请求里留下了哪些内容,以及它们是否都服务于某个具体判断"。

说明清楚,也不能改变工具能力

提示写着"列出所有函数",工具却只用正则匹配命名声明,模型无法通过更认真地回答补上完整语法分析。应先修正能力描述或替换实现,而不是让结果听起来更确定。

同样,如果文件读取失败,多贴几段泛化规则也不能补回真实内容。应报告失败、询问正确路径或使用允许的替代来源,而非编造一份符合格式的结果。

这两条有一个共同点:指令改变的是模型如何取舍信息,不改变宿主实际提供了什么。写得更清楚能让模型用好已有的能力,但凭空造不出没有的能力。

这一点可以推广成一条判断规则:当结果不对时,先分清是"信息没给够"还是"能力不够用"。 前者改提示词,后者改工具或换路径。

两者的症状不同。信息不足的典型表现是:模型做了一件相邻但不同的事,或者结论缺少依据但听起来仍然合理。能力不足的典型表现是:模型试图完成任务并在某个具体环节卡住——它反复用同一种不匹配的工具,或者输出一段明显不完整的结果。

对新手来说,第二种更容易被误判成第一种,因为"结果不对"的表面症状一样。区分办法是看过程:如果模型的尝试方向始终是对的方向、只是缺信息,那是信息问题;如果方向本身就不可能达到目标,那是能力问题。

Skill 提供方法,仍需要真实输入

Skill 可以把常见任务的方法和支持材料组织起来,例如先确认范围、再检查、最后解释限制。它有助于复用流程,不意味着每次都已经知道文件内容,也不自动让模型获得新工具。

Pi 在这里的具体做法值得说清楚,因为它直接决定了你该在 Skill 里放什么。

第一,Pi 只消费 frontmatter 的三个字段。 Agent Skills 规范定义了一整套字段,其中 name、description、disable-model-invocation 是 Pi 实际读取的;allowed-tools、license、compatibility、metadata 这些字段 Pi 并不消费。写进去不会报错,但也不会产生你以为的效果。

第二,启动时写进系统提示的只有三项。 Pi 在启动时扫描配置的 Skill 位置,把每个 Skill 的名称、描述和路径加进系统提示,不写入正文。正文要等模型判断任务匹配时,自己去读 SKILL.md。这就是"详细指导不占用上下文,直到需要时"的具体含义。

系统提示里实际出现的是这样一段:

xml
<available_skills>
  <skill>
    <name>pdf-tools</name>
    <description>Extract text and tables from PDF files. Use when reading, converting, or inspecting PDFs.</description>
    <location>/path/to/pdf-tools/SKILL.md</location>
  </skill>
</available_skills>

从这段结构能推出一个实用结论:description 决定了模型会不会去读这个 Skill。它必须同时说明"这个 Skill 做什么"和"什么时候该用",像"处理 PDF 的助手"这种写法提供的路由信息不够,模型没有理由在别的任务里也去加载它。

这个结构也解释了 Skill 相比直接把说明贴进提示词的差别。贴进提示词,说明会每一轮都占用上下文;做成 Skill,只有名称和描述常驻,正文按需加载。对于步骤多、但只在特定任务里才需要的方法,后者是更合适的选择。

反过来,如果一段流程几乎每个任务都要用,它就不适合做成 Skill——那属于"稳定规则",放进项目上下文文件更合适。选择依据是触发频率:偶尔才用的进 Skill,一直要用的进规则。

第三,disable-model-invocation: true 会让 Skill 完全不进系统提示,不只是隐藏命令入口。这类 Skill 只在你显式输入 /skill:name 时才可用——模型看不到它,也就永远不会主动去读它。

需要留意一个已知边界:模型可能没能加载本该匹配的 Skill。文档给出的应对办法是用 /skill:name 强制加载。在需要确保某个 Skill 生效的任务里,显式调用比依赖模型判断更可靠。

还有一件事和第 4 篇讲的"上下文从哪来"直接相关:项目里的 Skill 和用户目录里的 Skill 不是一回事。Pi 会递归发现包含 SKILL.md 的目录,也支持 ~/.agents/skills/ 和 .agents/skills/ 这两个 Agent Skills 规范位置;项目下的 .agents/skills/ 从当前目录逐级向上查找,到仓库根为止。

这个区分有实际后果。项目 Skill 可能指示模型运行脚本或修改文件,所以文档明确要求:在授予项目信任之前,先审阅不熟悉的 Skill 及其配套文件。一份 Skill 的正文就是交给模型的指令,而它只能写在你信任它之后才生效——这也是"规则"这一类输入需要审查的原因,和文件内容需要审查是同一个道理。

放在一起看,Skill 的三个机制各自解决一个容量问题:正文按需加载解决了"说明太长",description 作为路由依据解决了"不知道该用哪个",disable-model-invocation 解决了"不该被自动触发的流程别去触发"。三者配合,让 Skill 可以在项目里大量存在而不撑爆上下文。

判断一次输入里到底带了什么

这一节回答一个具体问题:怎么确认某条信息真的被发出去了,而不是看设置页上的显示。

设置页和提示词编辑框告诉你的是"你打算让系统做什么",不是"这次请求实际带了什么"。两者会因为模板展开、上下文文件发现、Skill 加载、工具集变化而产生差异。看实际请求,才是唯一可靠的核对方式。

这个区别在实践中最容易出问题的地方是"改了配置但行为没变"。配置改了要重新加载才生效——/reload 会重载 Skill、上下文文件和模板;在活动会话里直接编辑 AGENTS.md,模型这一轮仍然读的是旧版本。看到"我明明改了规则",先确认改动是否已经加载。

在 Pi 里,JSON 模式把这次运行的事件按行打印出来:

bash
pi --mode json "检查 src/greet.ts 中的命名 function 声明"

事件流里能看到这次会话产生的每一条消息,包括用户消息、助手消息和工具结果消息:

json
{"type":"message_start","message":{"role":"user","content":"检查 src/greet.ts 中的命名 function 声明","timestamp":1733234401000}}
{"type":"message_end","message":{"role":"user","content":"检查 src/greet.ts 中的命名 function 声明","timestamp":1733234401000}}

顺着同一个 toolCallId,还能把工具调用的三段串起来,确认"模型请求 → 工具执行 → 结果回填"确实发生过——这是第 1 篇说过的方法,在这一篇用来核对事实材料是否真的到位。

有两个地方值得单独确认。

系统提示和工具声明。 这些内容不随每条消息重发,而是在会话第一次请求时作为一条系统消息持久化。会话文件里的形态是这样:

json
{"type":"message","id":"a0b1c2d3","parentId":null,"timestamp":"2024-12-03T14:00:00.000Z","message":{"role":"system","content":"","sections":{"preamble":"You are an expert coding assistant...","tools":"<tools>\n- read: ...\n</tools>","cwd":"/project"},"toolsAdded":[{"name":"read","description":"...","parameters":{}}],"timestamp":1733234400000}}

sections 里每一项对应一段具名内容,toolsAdded 列出这次请求实际声明的工具。这段记录是"规则和工具到底发了什么"的唯一权威来源。想确认某个规则是否生效、某个工具是否注册,看它比看设置页可靠。

这次请求的输入体积。 上一篇提过,事件流里每个助手响应带一个 usage 字段,其中 input 是这次请求输入侧的实际用量。判断"我的提示词是不是太长",直接看这个数字,比估算准。

工具结果被截断了多少。 这是"事实材料"这一类输入最容易出问题的地方。工具结果进入上下文时会做截断:默认最多 2000 行、50 KB,grep 的匹配行最多保留 500 个字符。超出的部分会被裁掉,而这次裁剪不会在请求里留下标记——模型看到的就是被裁过的结果。

于是"我明明让工具读了这个文件,模型却说内容不全"就有了合理解释:不是模型漏看了,是工具结果本身在进入上下文前就被裁短了。核对时的做法是先看工具执行结果,再看实际进入上下文的长度,两者对不上就说明发生了截断。

在交互模式下,/session 可以查看会话文件位置、消息数、token 用量和费用;/reload 会在编辑 Skill 或上下文文件之后重新加载它们。

把这些核对点按一次排查的顺序整理一下:

要确认的事看哪里异常时的表现
规则发出去了吗
会话文件的系统消息 sections
规则完全没被遵守
工具注册了吗
系统消息的 toolsAdded
模型反复用不存在的工具
事实到位了吗
事件流的工具执行三段
结论缺少实际依据
事实完整吗
工具结果长度与上下文的对比
模型说内容不全
这次有多大
usage.input
上下文异常快满
我写的是原样吗
模板展开与扩展改写
模型复述了没写过的措辞

这张表的用法是:发现结果不对时,按顺序走一遍,先确定问题出在哪一层,再决定改哪里。跳过前面几层直接改提示词,是新手最常见的时间浪费——因为如果问题出在工具没注册,改多少遍提示词都没有用。

一个容易忽略的变形:输入在发出前会被改写

前面这些核对手段有一个前提:你以为发出去的就是你写下的。但宿主在组装用户消息时可能已经改写了它。

Pi 把这类改写集中在几个明确的环节:提示模板会在文本变成用户消息之前展开;选中的文件、图片、粘贴的文本和 shell 输出都可能成为消息内容;扩展可以在这个环节追加指令或转换上下文。

提示模板的展开规则是确定的。模板是一个带 frontmatter 的 Markdown 文件,文件名即命令名,正文里可以用 ${1:-默认值} 这类占位符接收参数:

markdown
---
description: Review staged git changes
argument-hint: "[focus]"
---
Review the staged changes. Focus on ${1:-correctness, security, and error handling}.

在编辑器里输入 /review concurrency,展开后才变成用户消息。这带来一个实际影响:最终发给模型的内容,可能比你输入的长得多。一个带默认值的模板会把参数缺省部分也一起填上;反过来,如果你想精确控制措辞,模板反而会插入你没写过的字。

核对时注意区分两层:编辑器里的输入是一层,实际进入消息的内容是另一层。怀疑"模型收到的和我写的不一样"时,把模板和上下文文件这两处一起看,比反复调整措辞更有效。

还有第三层:扩展。Pi 允许扩展在运行开始前注入消息或修改系统提示,这一步发生在输入处理和展开之后。扩展拿到的 systemPromptOptions 就是 Pi 自己用来搭系统提示的那份结构化数据——里面能看到当前启用的工具、项目上下文文件、已加载的 Skill,以及每一段提示的具体内容。

这个接口的存在说明了一件事:系统提示不是一块不可分割的文本,而是若干可分别增删的部分。修改时推荐改其中的某一段(sections、selectedTools、promptGuidelines),Pi 会对比新旧,只追加一条系统消息来修补变化的那几段;直接替换整段提示则会让 Provider 的缓存失效。

对初学者来说,知道扩展这个入口存在就够了:当你发现模型收到的规则和配置里写的不一致,除了模板和上下文文件,扩展也是一处值得排查的位置。

判断输入质量,看结果与证据

更长、更详细或更强硬的提示不一定更好。检查它是否减少了范围歧义、补齐必要证据、保留了重要限制,再观察不同任务是否仍能完成。

一个可以立刻上手的判断标准:把提示词的目标、范围、依据、输出四行分别遮住,看模型会在哪一行上出错。哪一行遮掉就出错,说明那一行确实在起作用;遮掉也没影响,说明它是冗余的,可以删掉。

这个方法的依据是"输入的每一部分都应对应一个具体的失败"。反过来也成立——如果你能描述出一个具体的失败模式,就知道该补哪一部分:

观察到的失败缺的是哪一部分
做了相邻但不同的事
目标或范围
结论听着有道理但对不上文件
依据
内容对但没法直接用
输出要求
越界改了不该改的东西
范围,或工具层面的授权
同一要求重复几遍仍然跑偏
大概率是工具能力不足,不是提示词问题

最后一行值得单独强调。"列出所有函数"这类要求,如果工具本身只能匹配命名声明,那么无论提示怎么改都补不齐语法分析——这是能力缺口,不是信息缺口,两者的处理方式完全不同。前者要改实现或换工具,后者才轮到改提示词。把两者混在一起,会把大量时间花在改写措辞上,而问题依然存在。

同理,"文件读取失败"也不能靠加规则解决。该做的是报告失败、询问正确路径,或者换用允许的替代来源。把一条不成立的规则写得更强硬,只会让失败看起来像是模型不听话。

这两张表合起来,就是一个可以重复使用的工作方法:先描述失败,再定位缺哪一部分,最后才动提示词。它比"改一版再试试"更可靠,因为每一步都有明确的判断依据,而不是靠试错碰运气。

也要承认它的边界。这套方法适用于能明确描述出失败的任务。如果任务本身就是开放的——"帮我改进这个项目"——那本来就说不清缺什么,此时更实际的做法是先做一次具体的检查,把"到底缺什么"变成可描述的失败,再回到上面这套流程。

初学阶段先用一个清楚的小任务建立这条关系;不要把未经验证的长提示词当作通用解决方案。实际提示与上下文调优,可以在后面的对照实践中进一步验证。

小结

给模型的信息可以按更新方式分成四类:稳定规则、当前目标、事实材料、输出要求。四类各自有不同的维护者,写错了也有不同的后果——这个区分比"提示词写得好不好"更能决定长期可维护性。

Skill 提供的是方法,不是事实。Pi 只读取 name、description、disable-model-invocation 三个字段,启动时只把名称、描述和路径写进系统提示,正文要等匹配时才加载。知道这个机制,才能判断"为什么模型没有用上这个 Skill",也能判断一个 Skill 该不该存在。

判断输入是否到位,靠看实际发出的请求,不靠看设置页。JSON 事件流、会话文件里的系统消息、工具结果的截断限制,加上模板展开和扩展改写,这几处是"你以为发的"和"实际发的"产生差异的全部位置。

这一篇的三个可操作结论,可以直接带走:

  1. 四类输入分开维护,并知道每一类由谁更新、写错了会有什么后果。
  2. Skill 只写方法和触发条件,事实材料永远实时获取。
  3. 核对时看请求,不看配置——从事件流和会话文件里确认规则、工具、事实、长度四件事。

下一篇会接着这个思路,介绍会话、记忆与检索:哪些内容值得跨轮次保留,以及一份摘要为什么可能过时。

资料依据