这是 AI 开发入门系列 的第 5 篇,讲 Prompt、Context、Harness 三种 Engineering 的分工与协作。相关文章还有:
一个问题
你刚开始学 AI 开发,想做一个代码审查助手——输入一段代码,让模型帮你找 bug。
你写了一段 prompt,模型确实返回了点东西。但你不太满意:它有时候发现不了明显的 bug,有时候说的「问题」根本不是问题,有时候漏掉了最关键的那行。
于是你反复改 prompt。加 role——「你是一个资深 Python 工程师」。加格式约束——「以 JSON 格式返回」。加 CoT——「请逐步分析」。改了好几个小时。
效果还是不行。
问题不在 prompt。你传给模型的只有一小段代码。它看不到这段代码依赖的函数,看不到项目里的类型定义,看不到相关的测试用例。它不知道 db.query 返回的是 dict 还是 User | None——它只能猜。猜错了就说「这里有 bug」,其实根本没有。
你需要的不是更好的 prompt,是更多东西塞给模型。
这就是这篇文章要讲的事。三个概念:
- Prompt Engineering——你让模型「听到」什么。只管指令文本本身。
- Context Engineering——你让模型「知道」什么。管指令 + 类型定义 + 相关代码 + 测试用例拼成的完整信息环境。
- Harness Engineering——你让系统「能做什么」。管工具、编排、记忆、评测——让模型从只能看代码的盒子,变成能跑 lint、能查类型、能验证自己判断的系统。
这三个概念是怎么来的
它们不是同一时间冒出来的。后一个的出现,都是为了解决前一个「管不了」的问题。
第一阶段:Prompt Engineering。 ChatGPT 刚火的时候,大家发现跟模型好好说话和随便说,效果差很多。「翻译这句话」效果不好,改成「你是专业翻译,请逐句翻译」就好不少。于是各种技巧出来了——Role Prompting、Few-shot、Chain-of-Thought。这个阶段你只能做一件事:改你发给模型的文本。
但 prompt 有一个天花板:模型回答不了它没见过的东西。你想让它审查你项目的代码?它压根不知道你的代码长什么样。Prompt 写得再精妙,也管不了「模型不知道什么」。
第二阶段:Context Engineering。 context window 从 4K 涨到 128K、1M tokens。大家开始想:既然能塞更多东西了,就把相关资料一起塞进去。这就是 Context Engineering 的起点——不只是写 prompt,还要管模型回答时能看到什么。RAG(检索增强生成) 在这个阶段爆发,从海量文档里检索出相关片段拼进 context。
但 context 也有天花板:你给模型塞再多资料,它还是只会「说」。你能让它真的跑一下 lint,验证自己找出的 bug 吗?做不到。Context 管「知道什么」,管不了「能做什么」。
第三阶段:Harness Engineering。 Tool Calling 和 Agent 出现后,模型不再只是一个会说话的盒子——它可以调函数、跑代码、查数据库。但这带来了一套新问题:tool 的 JSON Schema 怎么写模型才能调对?loop 什么时候停?模型调了一个发邮件的 tool,怎么加 guardrail 防止事故?这些回答不了「怎么问」也回答不了「给什么资料问」——它们需要一套新的基础设施。
简单记:Prompt 管怎么问,Context 管拿什么资料问,Harness 管除了问还能怎么验证、怎么行动。
Prompt Engineering
这是最外面一层:通过设计你发给模型的文本,让它输出你想要的东西。
道理很简单,但用一个对比例子就能看清楚区别。
普通版:
找出下面代码里的 bug。
def get_user_name(user_id):
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
return user.name
模型会返回一段自由文本,解释这个函数做了什么,提一两个模糊的建议。格式不统一,不好解析。
优化版:
你是一个资深 Python 代码审查工程师。
请分析以下代码,找出所有潜在 bug 和代码异味,以 JSON 格式返回。
返回格式:
{
"issues": [
{
"severity": "error | warning | info",
"line": 行号,
"category": "类型问题 | 空值风险 | 性能问题 | 安全风险",
"description": "具体描述"
}
]
}
代码:
def get_user_name(user_id):
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
return user.name
优化版加了三样东西:Role(明确身份)、格式约束(JSON schema)、分类体系(severity + category)。这三样就是 Prompt Engineering 的典型工作。
import openai
def review_code_with_prompt(code, system_prompt):
response = openai.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"代码:\n{code}"}
]
)
return response.choices[0].message.content
code = '''def get_user_name(user_id):
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
return user.name'''
# 普通版:输出是自由文本,不稳定
print(review_code_with_prompt(code, "找出下面代码里的 bug。"))
# 优化版:输出是结构化 JSON,可以直接用
system = """你是一个资深 Python 代码审查工程师。
以 JSON 格式返回,包含 severity、line、category、description 字段。"""
print(review_code_with_prompt(code, system))
Prompt Engineering 能做的事就是这些。它不管 db.query 返回什么类型,不管 user 可能为 None 的时候该不该处理——那些它管不了。
当你把 prompt 调到极限、模型还是发现不了关键问题的时候,问题出在这一层之外。你需要给模型更多资料。
Context Engineering
Prompt 只是 Context 的一个子集。Context 包含了模型推理时能访问的所有信息:
- System Prompt
- 对话历史
- RAG 检索到的文档片段
- Tool Call 返回的结果
- 项目元信息(框架、版本、语言)
回到代码审查的例子。你的 prompt 没问题,但模型只看到 get_user_name 这一个函数。它不知道 db.query 的返回类型,不知道 User model 长什么样。如果你把 context 补全——把 db.query 的类型注解、User 的 model 定义拼进去——模型就能更准地判断。
你写的不是 prompt 不够好,是 context 没给够。
context = system_prompt + "\n\n"
context += "### 相关类型定义\n"
context += "def db.query(sql, params) -> User | None: ...\n"
context += "class User:\n name: str\n\n"
context += "### 相关测试用例\n"
context += "def test_get_user_name_returns_none_when_user_not_found():\n"
context += " assert get_user_name(999) is None\n\n"
context += "### 待审查代码\n" + target_code
看到没?模型拿到了类型定义(User | None)后,就能判断出 return user.name 在 user 为 None 时会崩。没有这行类型定义的时候,它只能猜。
context window 是有限的。 128K 看起来大,一个中大型项目的代码很快就超了。你不能把整个项目塞进去。你得挑——当前函数 + 它直接依赖的类型 + 相关的测试用例。这就是 Context Engineering 的核心:有限的窗口里,放什么最值?
一个简单原则:跟当前任务最相关的,放最前或最后。 「Lost in the Middle」现象:放在 context 中间的信息,模型最容易忽略。
RAG 本质上就是 Context Engineering 的一个关键技术——怎么从海量代码库里选最相关的片段放进 context。这在我们系列的 RAG 是怎么工作的 里有完整拆解。
Harness Engineering
Context Engineering 解决的是「模型知道什么」。但你再给模型塞多少资料,它还是只会说,不会做。
Harness Engineering 就是让它动起来的。
┌─────────────────────────────────────────┐
│ Harness Engineering │ ← 编排、评测、监控、安全
│ ┌─────────────────────────────────┐ │
│ │ Context Engineering │ │ ← 代码上下文、类型信息、测试
│ │ ┌─────────────────────────┐ │ │
│ │ │ Prompt Engineering │ │ │ ← 审查指令、输出格式、角色
│ │ └─────────────────────────┘ │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
对于代码审查助手来说,Harness 管这些事:
Tool Definitions。 模型不能只靠看代码找 bug,它应该能真的跑一下 lint。你得给它定义 tool——run_linter(file_path)、get_type_info(function_name)、search_tests(function_name)。这和 function call 与 tool call 的区别 里讲的机制直接相关。
Orchestration Loop。 模型说「我觉得 user.name 可能空指针」——但它不能确定,得先调 get_type_info 查一下 db.query 的返回值——发现确实是 User | None——确认 bug——写入审查报告。这个「分析→调工具→观察结果→再分析」的循环,就是用 Python 从零写一个 LLM Agent 里手写的 Agent 循环。
Memory。 这轮审查的结果要不要记住?下次用户问「刚才那个 bug 修好了吗?」模型能想起来吗?
Guardrails & Safety。 用户传的代码里有没有 eval?有没有人想通过 prompt 注入绕过审查?模型输出的修复建议里会不会建议用 exec?
Evaluation。 审查助手的准确率是多少?跟人工 review 对比漏报了多少?改 prompt 之后效果变好了还是变差了?没有评测,你就不知道问题出在哪。
Observability。 每次请求花了多少 token?调了几次 tool?哪一步最慢?
一个常见的错误:你的代码审查助手漏掉 bug,你的第一反应是「我再调调 prompt」。但问题可能是它根本没法跑 lint 来验证,可能是 context 里缺了类型定义,可能是评测没跟上你连自己多不准都不知道。Prompt 是皮肤,Harness 是骨骼。骨骼歪了,换皮没用。
一个完整例子
用户传了一段代码要求审查:
def get_user_name(user_id):
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
return user.name
三层是怎么配合的:
Prompt 层:
你是资深 Python 代码审查工程师。
分析代码,找出所有潜在 bug,以 JSON 格式返回。
Context 层: 从项目里搜出 db.query 的类型注解(User | None)、User 的 model 定义、相关测试用例,拼成完整 context。
Harness 层: 模型分析后不确定 user.name 有没有空指针风险 → 调 get_type_info("db.query") → 返回 User | None → 确认这是 bug。
def review_code(user_code):
# Prompt 层:审查指令
system = "你是资深 Python 代码审查工程师。以 JSON 格式返回 bug 列表。"
# Context 层:拼装上下文
ctx = system + "\n\n"
ctx += "### 类型定义\n" + get_type_info("db.query") + "\n\n" # → User | None
ctx += "### 待审查代码\n" + user_code
# Harness 层:调 LLM + tool call
response = openai.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": ctx}],
tools=[{
"type": "function",
"function": {
"name": "get_type_info",
"description": "查询函数或方法的返回类型",
"parameters": {
"type": "object",
"properties": {
"function_name": {"type": "string"}
}
}
}
}]
)
# 模型判断需要查类型 → 调 get_type_info → 拿到 User | None → 继续分析
msg = response.choices[0].message
if msg.tool_calls:
# 执行 tool,结果再喂回模型
tool_result = execute_tool(msg.tool_calls[0])
messages.append({"role": "tool", "content": tool_result})
# 第二轮:模型拿到 User | None,确认空指针风险
final = openai.chat.completions.create(
model="gpt-4o",
messages=messages
)
return final.choices[0].message.content
流程是先拼 context(Context 层),再交给模型判断(Prompt 层),模型不确定就调 tool 验证(Harness 层),拿到结果再继续判断。每一层各司其职。
常见误区
「我是在做 Prompt Engineer。」
如果你在管知识库、调 tool schema、写评测脚本——你做的远不止 Prompt Engineering。把所有问题归因于「prompt 没写好」,会让你忽略真正的瓶颈。
「Context 塞满就行,越多越好。」
塞满 context window 不等于模型会注意到所有内容。Lost in the Middle:放在中间的类型定义,模型可能根本没看。Context Engineering 的核心是精选,不是堆量。
「Harness 就是给模型加几个 tool。」
真正要处理的是:tool 调用失败怎么重试?并发 tool call 的返回顺序怎么处理?多轮对话的状态存在哪?这跟「后端就是写 API」是一个道理——API 写出来只是第一步,缓存、限流、日志、重试都在后面等着。
「三个概念是互斥的,学好一个就行。」
它们是嵌套的:Prompt ⊂ Context ⊂ Harness。你的代码审查助手效果好 = 清晰的审查格式 × 关键的类型信息没丢 × 能跑 lint 验证。
总结
三个概念不是选择题。你不可能「只做 Prompt Engineering、不管 Context 和 Harness」——因为 Prompt 本身就跑在 Context 里,而 Context 靠 Harness 来组装和调度。
下次你调 prompt 调到抓狂,先停一下。想想你的代码审查助手到底缺了什么——是 prompt 说得不够清楚,是 context 里少了一行关键的 User | None,还是它压根没机会跑一次 lint 来验证自己的判断。
大多数时候,问题都不在 prompt。