返回博客列表
技术文章LLMAIAgentPrompt EngineeringContext Engineering架构设计

Prompt、Context、Harness——三种 Engineering 到底有什么不同

2026年06月🇨🇳 中文

这是 AI 开发入门系列 的第 5 篇,讲 Prompt、Context、Harness 三种 Engineering 的分工与协作。相关文章还有:

  1. 四种主流的 Agent 架构
  2. function call 与 tool call 的区别
  3. 用 Python 从零写一个 LLM Agent
  4. RAG 是怎么工作的

一个问题

你刚开始学 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.nameuser 为 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。