副标题:Agent 与上下文工程 · 选型与落地
本文为 vergex.cn 原创首发,转载请注明出处。
你团队里那位最会写 prompt 的同事,可能正在用“提示词工程”的方法,去解决一个本应由“上下文工程”负责的问题:系统不是“不会说”,而是“没看到该看的东西”。
一、先对齐概念:两者不是“同一个东西的两套叫法”
提示词工程(Prompt Engineering)优化的是单次请求里模型“看到”的指令本身——怎么措辞、加多少示例、限定什么格式、如何约束输出。上下文工程(Context Engineering)优化的是进入模型窗口之前的全部信息:哪些来源值得被带进来、以什么结构组织、占多少 token、失败后如何降级、答案错了怎么追溯。
我经常用下面这张表向技术委员会解释资源分配。表中的评估维度可直接复用到内部评审,完整权重和打分项的完整版已在 VergeX 站点放出。
| 维度 | 提示词工程 | 上下文工程 |
|---|---|---|
| 优化对象 | 单条/单次指令的语义表达 | 进入模型窗口的全部信息结构与来源 |
| 核心手段 | 指令模板、少样本示例、角色设定、输出格式 | 召回策略、分块边界、元数据、预算分配、失效与版本 |
| 成功标准 | 单次回复质量(相关性、格式、幻觉率) | 端到端任务完成率、上下文命中率、token 利用率、可追溯性 |
| 组织职责 | 算法/产品工程师 | 平台/数据/Agent 架构团队 |
| 迭代周期 | 小时级 | 周/月级,随业务知识库变化 |
关键结论是:提示词工程解决“表达问题”,上下文工程解决“输入供给问题”。 当团队反复调 prompt 却效果停滞时,大概率是上下文供给没做好,而不是 prompt 写得不够“魔法”。
二、上下文工程的三层操作对象
我把上下文工程拆成三层,每层对应不同的落地抓手。
来源层(Source Layer) 决定“什么东西有资格进入上下文”。不是知识库越大越好,而是要建立召回、过滤、排序的闭环。常见做法包括:按业务实体做索引隔离,避免把 A 产品的文档塞进 B 产品的对话;给每个 chunk 打上版本、生效时间、权限标签;在召回后做二次重排(rerank),而不是直接取 top-k。
结构层(Structure Layer) 决定“进来后怎么呈现”。模型对边界标记非常敏感。推荐用带显式标签的容器结构,例如 <source id="..." title="..."> 包裹每个知识片段,而不是用换行或 Markdown 列表简单拼接。结构层还要处理冲突:当两个来源结论矛盾时,是通过元数据标注优先级,还是在 prompt 里让模型自行判断?这需要在工程层面显式定义策略。
运行层(Runtime Layer) 决定“窗口预算怎么花”。总窗口不等于可用上下文长度,必须预留生成 token、系统指令开销、少量安全余量。运行层还要处理失效:召回为空怎么办、上下文超长时先丢哪一类信息、模型输出要求引用但来源缺失时如何兜底。
三、提示词工程的边界在哪里
提示词工程在三种场景下回报率最高:一是任务语义明确、但需要约束输出形式;二是通过 few-shot 让模型快速对齐隐式风格;三是设置角色和拒绝策略,降低安全与合规风险。
但它有明显边界。当知识不在模型参数里、且会动态变化时,仅靠 prompt 调优无法解决。 比如客服 Agent 需要引用昨日刚更新的退款政策,你再怎么优化措辞,模型也拿不到这条信息。此时问题的根因是上下文供给链路,而不是 prompt 表达。
另一个边界是“上下文噪声”。如果一次请求里塞进了 20 个不相关 chunk,再好的 prompt 也难以让模型忽略它们。LLM 对位置有显著偏好,中间内容容易被“丢失”。这种情况下,与其继续打磨 prompt,不如先优化召回与重排。
判断何时该从 prompt 工程切换到上下文工程,有一个简单标准:连续三轮 prompt 调优后,端到端指标提升不到 5%,就把精力转向上下文管线。
四、从“单轮调优”到“上下文管线”的落地路径
L2 管理者推动这件事,建议分四步走,不要一开始就重写整个 Agent。
第一步,建立无上下文的 prompt 基线。 用固定知识写死 prompt,确认模型在信息完备时的表现天花板。这一步的输出是“理论上最好能做到什么程度”,也是后续所有对比的 benchmark。
第二步,接入最小可用上下文。 引入向量检索或规则召回,把 top-3 到 top-5 的 chunk 注入对话。此时先不要追求复杂策略,重点验证:加了上下文后,答案是否真的在引用这些来源,而不是用参数记忆胡编。
第三步,做动态预算与重排。 根据问题类型分配不同上下文长度:事实性问题给短而精的 chunk,复杂分析任务给长上下文。引入 rerank 模型或规则打分,把真正相关的片段提前。这一步开始需要 token 成本核算。
第四步,建立评估飞轮。 用固定测试集持续跑端到端指标,包括答案正确率、来源引用命中率、上下文 token 利用率。推荐把核心用例写成 pytest,纳入 CI:
python -m pytest tests/context_pipeline/ --tb=short -q
这个飞轮跑起来后,prompt 工程和上下文工程就可以并行迭代,而不是互相甩锅。
五、一个可复现的上下文注入评估脚本
下面这段代码可以直接跑,用来评估“上下文装了多少、还剩多少预算、是否发生截断”。它使用 tiktoken 做 token 计数,用 openai 的 SDK 发起请求。你需要先安装依赖并设置 OPENAI_API_KEY:
pip install openai tiktoken
export OPENAI_API_KEY="sk-..."
import os
import tiktoken
from openai import OpenAI
MODEL = "gpt-4o"
ENCODING = tiktoken.encoding_for_model(MODEL)
MAX_CONTEXT = 128_000
RESERVE_GENERATION = 4_096
RESERVE_SYSTEM_OVERHEAD = 1_024
def token_count(text: str) -> int:
return len(ENCODING.encode(text, disallowed_special=()))
def assemble_context(retrieved_chunks, query, instructions):
system_prompt = f"""You are a strict Q/A assistant.
{instructions}
Use ONLY the <source> chunks below to answer. Cite using [id]."""
context_block = "\n\n".join(
f'<source id="{c["id"]}" title="{c["title"]}">\n{c["text"]}\n</source>'
for c in retrieved_chunks
)
user_prompt = (
f"<question>\n{query}\n</question>\n\n"
f"<context>\n{context_block}\n</context>"
)
return system_prompt, user_prompt
def fit_context(system_prompt, user_prompt, max_context_tokens):
system_tokens = token_count(system_prompt)
user_tokens = token_count(user_prompt)
total = system_tokens + user_tokens + RESERVE_SYSTEM_OVERHEAD
if total > max_context_tokens:
allowed_user = max_context_tokens - system_tokens - RESERVE_SYSTEM_OVERHEAD
encoded = ENCODING.encode(user_prompt, disallowed_special=())
truncated = ENCODING.decode(encoded[:allowed_user])
user_prompt = truncated + "\n...[truncated]"
total = system_tokens + token_count(user_prompt) + RESERVE_SYSTEM_OVERHEAD
return system_prompt, user_prompt, total
def run(system_prompt, user_prompt):
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
temperature=0.1,
max_tokens=RESERVE_GENERATION,
)
return response.choices[0].message.content
if __name__ == "__main__":
chunks = [
{"id": "doc-1", "title": "退款政策", "text": "用户可在 7 天内申请无理由退款。"},
{"id": "doc-2", "title": "VIP 退款", "text": "VIP 用户享有 14 天超长退款期。"},
]
sys_p, user_p = assemble_context(
chunks,
query="VIP 退款期限是多久?",
instructions="Answer in Chinese. If the answer is not in the context, say '未找到依据'."
)
sys_p, user_p, total = fit_context(
sys_p, user_p, MAX_CONTEXT - RESERVE_GENERATION
)
budget_left = MAX_CONTEXT - total - RESERVE_GENERATION
print(f"上下文占用 token: {total}")
print(f"剩余预算(含生成预留): {budget_left}")
# 取消注释下一行即可真正调用模型
# print(run(sys_p, user_p))
这个脚本的核心价值不在“能跑”,而在它把上下文预算算清楚了。总窗口减去生成预留和系统开销,才是真正能分给上下文的部分。 很多项目的问题不是 prompt 写得不好,而是上下文把窗口塞太满,导致模型没空间组织答案。
六、两个最常见的“隐形坑”
坑一:直接把召回的 chunk 拼进 messages,不加来源标签和边界。 我见过不少团队把检索结果用 "\n---\n".join(chunks) 丢进 user message,然后要求模型“给出引用”。结果模型要么杜撰引用编号,要么把多个来源混在一起张冠李戴。原因是模型无法可靠地区分“这是来源 A 的文本”和“这是来源 B 的文本”。解法很简单:给每个 chunk 加显式标签,例如 <source id="doc-2">...</source>,并在 prompt 里规定引用格式。没有结构化边界,就不要要求结构化引用。
坑二:把上下文窗口填到 100%,没有预留生成 token 和系统开销。 有些同学看到模型支持 128K 上下文,就以为可以塞 128K 的文本。实际上,系统指令、角色设定、输出格式示例都会占 token;模型回复本身也需要预留空间。如果你在运行时把上下文塞得太满,API 会触发截断,或者模型在末尾被迫中断。建议用上面的脚本算清楚:可用上下文长度 = 模型总窗口 - 最大生成长度 - 系统开销 - 10% 安全余量。宁可少塞一点,也要保证生成空间完整。
这两个坑之所以“隐形”,是因为它们不会直接报错,只会表现为“模型偶尔答错”“引用不稳定”“长文本回答被截断”,从而让人误以为要继续调 prompt。
七、给 L2 管理者的落地清单
如果你是技术管理者,想要推动团队从 prompt 工程走向上下文工程,建议先检查下面几项:
- 团队是否有统一的上下文注入规范,包括 chunk 边界、元数据字段、引用格式?
- 每次请求是否有明确的 token 预算分配策略,而不是“能塞多少塞多少”?
- 是否建立了端到端评估集,覆盖“上下文命中”“来源引用正确”“无上下文时拒答”三类用例?
- prompt 迭代和上下文迭代是否由不同角色负责,还是全压在一个人身上?
- 生产环境是否对上下文来源做了版本和生效时间管理,避免新旧政策冲突?
文末再提一句:前面那张对照表只是简化版,包含完整评分权重、检查项和部门分工的《上下文工程 vs 提示词工程 评估对照表》完整版已放在 VergeX 站点,可直接拿来开会用。
反模式清单:
- 让业务团队直接在 prompt 里拼接长文本,而不是走上下文管线。
- 用离线 prompt 评测分数代替端到端任务完成率。
- 把上下文窗口填满后再交给模型,不预留生成 token。
- 要求模型给出引用,却不给 chunk 标注来源。
- 不同环境使用不同分块策略,却不做回归测试。
- 把动态知识更新当作 prompt 调优问题处理。
核心总结:提示词工程决定单次交互能有多好,上下文工程决定这个好能被规模化、可复现地交付多少次。

