Prompt 工程已死,上下文工程当立

Posted on Fri 16 January 2026 in AI

开篇:别让你的 AI "断章取义"

最近你是不是也有这种感觉:精心雕琢了半小时的 Prompt,像写咒语一样把 system prompt 调教得花里胡哨,结果 AI 给你的回答还是像个刚入职的实习生——有礼貌,但没用。

醒醒吧,Prompt 工程的红利期已经过去了。 现在是 "上下文工程 (Context Engineering)" 的天下。

想象一下,你是个大厨。Prompt 是菜谱,AI 是灶台。如果你的冰箱里(Context)只有泡面,就算你是米其林三星大厨,你也做不出佛跳墙。

上下文工程,就是那个负责把"顶级食材"塞进 AI 嘴里的学问。

今天,我们来聊聊什么是上下文工程,它跟 RAG 到底是什么关系,最后用一个"AI 面试官"的完整实例(含 LangChain 代码),把这套方法论落地。


一、什么是"上下文工程"?

简单说,上下文工程 (Context Engineering) = 把正确的信息,以正确的格式,在正确的时机,喂给 AI

它不仅仅是 RAG(检索增强生成),它是 RAG 的上位概念。如果把 AI Agent 比作一个去考试的学生: * Prompt 是考卷上的题目。 * Context 是你允许他带进考场的全部"小抄"——包括教科书、笔记、计算器、甚至能打电话问场外观众。

上下文工程的五大核心组件:

组件 类比 说明
RAG 外部图书馆 AI 不知道的,去数据库里查
Memory (短期) 对话记录 刚才聊了啥?
Memory (长期) 用户档案 用户的喜好是啥?上次会议的结论是啥?
Tools AI 的手脚 不仅给信息,还能干活(查天气、发邮件、调 API)
Prompts 指挥棒 告诉 AI 怎么用上面这些东西

为什么它比 Prompt 更重要? 因为大模型本质上是一个概率预测机器。你喂给它的上下文越精准、越相关,它"猜"对下一个字的概率就越高。与其花时间教它"如何思考",不如直接给它"思考素材"。


二、上下文工程 vs RAG:一个是爹,一个是儿子

很多人把"上下文工程"和"RAG"混为一谈。这就像把"烹饪"等同于"买菜"。买菜(RAG)只是烹饪(Context Engineering)的一个环节。

RAG 只解决了一个问题:信息检索

RAG 的经典流程:

用户提问 -> 向量化 -> 从数据库检索 Top-K 相关文档 -> 塞进 Prompt -> LLM 生成答案

RAG 的局限性: 1. 只管"取",不管"选":它把检索到的 Top-K 一股脑塞给 LLM,不管这些内容是否真的有用。 2. 没有"记忆":每次对话都是无状态的,不知道用户是谁,不知道之前聊过什么。 3. 不能"干活":只能读信息,不能写信息,不能调 API。 4. 缺乏"判断":不会根据任务类型动态调整上下文策略。

上下文工程:把 RAG 当作一个"工具"来调度

上下文工程的视角更高。它会问: * 这个任务需要 RAG 吗?还是只需要调用一个 API? * 检索出来的内容,哪些该留,哪些该丢? * 用户的历史偏好,要不要加进去? * 上下文快爆了(Context Overflow),怎么压缩?

用一张图说清楚

context-engineering

三、实战:一个"AI 面试官" Agent 的上下文工程

光说不练假把式。我们来构建一个真正有挑战性的场景:你是一家公司的招聘负责人,想做一个 AI 面试官,它能根据岗位 JD、候选人简历、公司面试题库,进行一场专业的技术面试。

这个场景比"聊天机器人"复杂多了。它需要: * RAG:从题库里检索相关问题。 * Memory:记住候选人这场面试的表现、回答了哪些题。 * Tools:调用日程 API 安排下一轮面试、调用邮件 API 发送结果。 * Prompts:根据面试阶段(开场白、技术提问、追问、总结)切换不同的角色。

Step 1: 梳理上下文来源

上下文类型 来源 时效性 格式
岗位 JD HR 系统 静态 JSON
候选人简历 ATS 系统 静态 Markdown/JSON
面试题库 内部知识库 动态 Elasticsearch
候选人历史回答 本场对话 实时 对话历史
面试评分标准 知识库 静态 JSON

Step 2: 定义工具 (Tools)

tools = [
    {
        "name": "search_question_bank",
        "description": "从面试题库中检索与指定技能相关的面试题。返回题目、难度、参考答案要点。",
        "parameters": {
            "skill": "技能标签,如 'Python', 'System Design', 'Kubernetes'",
            "difficulty": "难度等级:easy, medium, hard",
            "count": "返回题目数量"
        }
    },
    {
        "name": "evaluate_answer",
        "description": "评估候选人的回答质量,返回得分(1-10)和简评。",
        "parameters": {
            "question": "原始问题",
            "candidate_answer": "候选人的回答",
            "reference_points": "参考答案要点"
        }
    },
    {
        "name": "schedule_next_round",
        "description": "为候选人安排下一轮面试。",
        "parameters": {
            "candidate_id": "候选人ID",
            "interviewer_id": "面试官ID",
            "time_slot": "时间段"
        }
    },
    {
        "name": "send_rejection_email",
        "description": "发送委婉的拒绝邮件。",
        "parameters": {
            "candidate_email": "候选人邮箱",
            "feedback": "反馈内容"
        }
    }
]

Step 3: 设计记忆 (Memory)

长期记忆 (Long-term Memory):存储在数据库里,跨 Session 有效。

{
  "candidate_id": "C-2026-001",
  "name": "张三",
  "position": "Senior Backend Engineer",
  "skills": ["Go", "Kubernetes", "PostgreSQL"],
  "previous_interviews": [
    {"date": "2026-01-10", "round": "Phone Screen", "result": "Pass", "notes": "基础扎实"}
  ]
}

短期记忆 (Short-term Memory):本场面试的上下文。

{
  "session_id": "S-20260116-001",
  "questions_asked": [
    {"q": "请解释 Go 的 GMP 模型", "a": "...", "score": 8}
  ],
  "current_phase": "technical_deep_dive",
  "overall_impression": "沟通清晰,技术细节掌握较好"
}

Step 4: 上下文组装 (Context Assembly) —— 核心环节

这是上下文工程的精髓。不是把所有信息都塞进去,而是根据当前阶段,动态选择最相关的信息

def assemble_context(session_state: dict, user_message: str) -> str:
    """动态组装上下文"""
    context_parts = []

    # 1. 根据阶段选择不同的 System Prompt
    if session_state["current_phase"] == "opening":
        context_parts.append(PROMPT_OPENING)  # "你是友好的面试官,先做自我介绍..."
    elif session_state["current_phase"] == "technical_deep_dive":
        context_parts.append(PROMPT_TECHNICAL)  # "你是严谨的技术面试官,追问细节..."
    elif session_state["current_phase"] == "closing":
        context_parts.append(PROMPT_CLOSING)  # "总结面试,询问候选人问题..."

    # 2. 注入岗位 JD(压缩版,只保留关键要求)
    jd_summary = summarize_jd(session_state["job_description"])
    context_parts.append(f"【岗位要求摘要】\n{jd_summary}")

    # 3. 注入候选人简历(重点突出匹配项)
    resume_highlights = extract_highlights(
        session_state["candidate_resume"],
        session_state["job_description"]
    )
    context_parts.append(f"【候选人亮点】\n{resume_highlights}")

    # 4. 注入本场面试历史(只保留最近 3 轮问答,防止 Context Overflow)
    recent_qa = session_state["questions_asked"][-3:]
    context_parts.append(f"【本场面试记录】\n{format_qa(recent_qa)}")

    # 5. 如果需要出新题,调用 RAG 检索题库
    if should_ask_new_question(session_state, user_message):
        skill_to_test = infer_skill_gap(session_state)
        questions = search_question_bank(skill=skill_to_test, difficulty="medium", count=2)
        context_parts.append(f"【备选题目】\n{format_questions(questions)}")

    return "\n\n".join(context_parts)

Step 5: 看看效果

面试开场

AI 面试官:你好张三,我是今天的技术面试官。看了你的简历,你在云原生领域有不少经验。
          我们今天主要聊聊 Kubernetes 和 Go 相关的话题。准备好了吗?

技术追问

候选人:Go 的 GMP 模型就是 Goroutine、Machine 和 Processor...

AI 面试官:[内部调用 evaluate_answer,得分 7/10,缺少对抢占式调度的描述]
          不错的解释。我追问一下,Go 1.14 之后引入的异步抢占机制,解决了什么问题?

候选人:呃...这个我不太确定...

AI 面试官:[内部记录:抢占式调度-不熟悉]
          没关系,这是一个比较深入的点。我们换个方向,聊聊你在上家公司做的 K8s 项目...

面试结束

AI 面试官:[内部调用:汇总得分、生成评语、决定是否进入下一轮]
          好的张三,今天的面试就到这里。总体来说,你的基础扎实,项目经验也很匹配。
          HR 会在三个工作日内联系你。你有什么问题想问我吗?

[内部调用 schedule_next_round(...)]

四、避坑指南:Context 里的那些雷

  1. 上下文中毒 (Context Poisoning): 别以为喂得越多越好。塞进去一堆无关信息(Context Confusion),AI 会晕的。记住:"Less is More",精准检索 > 海量检索

    反面案例:把候选人所有历史面试记录(包括 5 年前的)都塞进去,结果 AI 说"根据你 2019 年的面试表现..."——尴尬。

  2. 工具爆炸 (Tool Explosion): 别给 Agent 一百个工具。它会不知道选哪个。如果你有很多工具,请用 RAG 的方式去检索工具描述,动态挂载最相关的几个。

  3. 格式即正义 (Format Matters): 给 AI 看的资料,最好是结构化的(Markdown, JSON)。别给它看 OCR 扫描出来的乱码 PDF。

  4. 上下文窗口溢出 (Context Overflow): LLM 的上下文窗口有限(即使是 Claude 的 200K 也不是无限的)。当对话很长时,需要:

    • 压缩历史:用 LLM 把前面的对话总结成摘要。
    • 滑动窗口:只保留最近 N 轮详细记录。
    • 分层存储:重要信息放长期记忆(数据库),日常对话放短期记忆(内存)。

五、上下文工程的 Checklist

在你构建下一个 AI Agent 之前,问自己这几个问题:

  • [ ] 信息来源识别:我的 Agent 需要哪些信息?它们在哪里?(数据库、API、用户输入)
  • [ ] 元数据设计:我的数据有没有时间戳、标签、类型等元信息,方便过滤?
  • [ ] 工具定义:我给 Agent 定义了哪些工具?描述是否清晰到 LLM 能理解?
  • [ ] 记忆策略:短期记忆怎么管理?长期记忆存在哪?如何防止"记忆泄漏"?
  • [ ] 上下文组装:我是否在根据任务类型动态组装上下文,而不是一股脑全塞?
  • [ ] 压缩策略:当上下文快爆时,我有没有摘要/滑动窗口机制?
  • [ ] 评估与监控:我能不能看到 Agent 每次实际用了哪些上下文?效果如何?

总结:从 Prompt Engineer 进阶到 Context Engineer

维度 Prompt 工程 上下文工程
关注点 "怎么说" "说什么"
核心技能 写好 System Prompt 设计数据流、组装策略
解决问题 LLM 输出格式、角色扮演 LLM 知识缺失、信息时效性、多轮对话一致性
复杂度 高(涉及 RAG、Memory、Tools 等多个系统)
未来趋势 基本功,门槛越来越低 核心竞争力,差异化来源

一句话总结:未来的 AI 开发,写 Prompt 只是基本功。真正的核心竞争力,在于你能不能构建、管理、优化 AI 的上下文

下次你的 AI 还在胡说八道时,别急着改 Prompt。去看看你的上下文管道,是不是该做次大扫除了?


六、完整代码:用 LangChain 实现 AI 面试官

Talk is cheap, show me the code. 下面是一个可以直接运行的 LangChain 原型,展示了上下文工程的核心思想。

安装依赖

pip install langchain langchain-openai chromadb pydantic

完整代码

"""
AI 面试官 Agent - 上下文工程实战
展示 RAG + Memory + Tools + Dynamic Prompts 的组合使用
"""
import os
from typing import List, Dict, Optional
from pydantic import BaseModel, Field

from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain_core.tools import tool
from langchain_community.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.agents import AgentExecutor, create_openai_tools_agent

# ========== 1. 数据模型 ==========
class Candidate(BaseModel):
    """候选人档案(长期记忆)"""
    id: str
    name: str
    position: str
    skills: List[str]
    resume_text: str
    previous_interviews: List[Dict] = []

class InterviewSession(BaseModel):
    """面试会话(短期记忆)"""
    session_id: str
    candidate: Candidate
    questions_asked: List[Dict] = []
    current_phase: str = "opening"  # opening, technical, behavioral, closing
    scores: List[int] = []

# ========== 2. 模拟数据 ==========
# 面试题库(实际项目中存在 ES 或向量数据库)
QUESTION_BANK = [
    {"id": "q1", "skill": "Go", "difficulty": "medium", 
     "question": "请解释 Go 的 GMP 调度模型", 
     "key_points": ["Goroutine", "Machine", "Processor", "work stealing"]},
    {"id": "q2", "skill": "Go", "difficulty": "hard", 
     "question": "Go 1.14 引入的异步抢占机制解决了什么问题?", 
     "key_points": ["死循环", "GC STW", "信号抢占"]},
    {"id": "q3", "skill": "Kubernetes", "difficulty": "medium", 
     "question": "Pod 的生命周期有哪些阶段?", 
     "key_points": ["Pending", "Running", "Succeeded", "Failed", "Unknown"]},
    {"id": "q4", "skill": "System Design", "difficulty": "hard", 
     "question": "设计一个高可用的消息队列系统", 
     "key_points": ["持久化", "复制", "消费确认", "死信队列"]},
]

# 模拟候选人
SAMPLE_CANDIDATE = Candidate(
    id="C-2026-001",
    name="张三",
    position="Senior Backend Engineer",
    skills=["Go", "Kubernetes", "PostgreSQL"],
    resume_text="""
    张三 - 高级后端工程师
    工作经验:5年
    技能:Go, Kubernetes, PostgreSQL, Redis
    项目经历:
    1. 某大厂微服务改造项目,负责服务拆分和 K8s 迁移
    2. 高并发订单系统,QPS 达到 10w+
    """
)

# ========== 3. 定义工具 (Tools) ==========
@tool
def search_question_bank(skill: str, difficulty: str = "medium") -> str:
    """从面试题库中检索与指定技能相关的面试题。

    Args:
        skill: 技能标签,如 'Go', 'Kubernetes', 'System Design'
        difficulty: 难度等级:easy, medium, hard
    """
    results = [q for q in QUESTION_BANK 
               if q["skill"].lower() == skill.lower() 
               and q["difficulty"] == difficulty]
    if not results:
        results = [q for q in QUESTION_BANK if q["skill"].lower() == skill.lower()]

    if results:
        q = results[0]
        return f"题目: {q['question']}\n参考要点: {', '.join(q['key_points'])}"
    return "未找到相关题目"

@tool
def evaluate_answer(question: str, candidate_answer: str, key_points: str) -> str:
    """评估候选人的回答质量,返回得分(1-10)和简评。

    Args:
        question: 原始问题
        candidate_answer: 候选人的回答
        key_points: 参考答案要点,逗号分隔
    """
    # 实际项目中,这里会调用 LLM 进行评估
    # 这里简化为关键词匹配
    points = key_points.lower().split(",")
    answer_lower = candidate_answer.lower()
    matched = sum(1 for p in points if p.strip() in answer_lower)
    score = min(10, max(1, int(matched / len(points) * 10) + 3))

    if score >= 8:
        comment = "回答完整,掌握扎实"
    elif score >= 6:
        comment = "基本正确,但缺少部分细节"
    else:
        comment = "理解不够深入,建议加强"

    return f"得分: {score}/10\n评语: {comment}"

@tool
def record_interview_note(note: str) -> str:
    """记录面试笔记,用于后续评估。

    Args:
        note: 面试笔记内容
    """
    # 实际项目中会存入数据库
    return f"已记录笔记: {note}"

@tool
def decide_next_step(average_score: float) -> str:
    """根据平均得分决定下一步行动。

    Args:
        average_score: 候选人的平均得分(1-10)
    """
    if average_score >= 7:
        return "建议进入下一轮面试,安排与 Tech Lead 的深度技术面"
    elif average_score >= 5:
        return "建议安排补充面试,重点考察薄弱环节"
    else:
        return "建议发送婉拒邮件,感谢候选人的时间"

# ========== 4. 动态 Prompt 模板 ==========
PROMPTS = {
    "opening": """你是一位友好专业的技术面试官。现在是面试开场阶段。

【候选人信息】
{candidate_info}

【岗位要求】
{job_requirements}

请先做简短的自我介绍,然后根据候选人的简历,找一个话题破冰,让候选人放松下来。
注意:不要一开始就问技术难题。""",

    "technical": """你是一位严谨的技术面试官。现在进入技术深挖阶段。

【候选人信息】
{candidate_info}

【已问问题和回答】
{interview_history}

【待考察技能】
{skills_to_test}

请根据候选人的技能背景,使用 search_question_bank 工具检索合适的面试题。
根据候选人的回答,使用 evaluate_answer 工具进行评估。
如果回答不够深入,可以追问细节。""",

    "closing": """你是一位专业的面试官。现在是面试总结阶段。

【面试记录】
{interview_history}

【得分统计】
{score_summary}

请:
1. 使用 decide_next_step 工具根据平均得分决定下一步
2. 给候选人一个简短的反馈(不透露具体分数)
3. 询问候选人是否有问题要问"""
}

# ========== 5. 上下文组装器 ==========
class ContextAssembler:
    """上下文工程的核心:动态组装上下文"""

    def __init__(self, session: InterviewSession):
        self.session = session

    def get_candidate_info(self) -> str:
        c = self.session.candidate
        return f"""
姓名: {c.name}
应聘岗位: {c.position}
技能: {', '.join(c.skills)}
简历摘要: {c.resume_text[:500]}...
"""

    def get_interview_history(self) -> str:
        if not self.session.questions_asked:
            return "暂无"

        history = []
        for i, qa in enumerate(self.session.questions_asked[-5:], 1):  # 只取最近5条
            history.append(f"{i}. Q: {qa.get('question', 'N/A')}")
            history.append(f"   A: {qa.get('answer', 'N/A')[:200]}...")
            if 'score' in qa:
                history.append(f"   得分: {qa['score']}/10")
        return "\n".join(history)

    def get_score_summary(self) -> str:
        if not self.session.scores:
            return "暂无得分记录"
        avg = sum(self.session.scores) / len(self.session.scores)
        return f"已评估 {len(self.session.scores)} 道题,平均得分: {avg:.1f}/10"

    def assemble(self, phase: str = None) -> str:
        """根据阶段动态组装 Prompt"""
        phase = phase or self.session.current_phase
        template = PROMPTS.get(phase, PROMPTS["technical"])

        return template.format(
            candidate_info=self.get_candidate_info(),
            job_requirements="5年以上 Go 开发经验,熟悉 K8s,有大规模分布式系统经验",
            interview_history=self.get_interview_history(),
            skills_to_test=", ".join(self.session.candidate.skills),
            score_summary=self.get_score_summary()
        )

# ========== 6. 主程序 ==========
def create_interview_agent():
    """创建面试官 Agent"""
    llm = ChatOpenAI(model="gpt-4-turbo", temperature=0.3)
    tools = [search_question_bank, evaluate_answer, record_interview_note, decide_next_step]

    prompt = ChatPromptTemplate.from_messages([
        ("system", "{system_prompt}"),
        MessagesPlaceholder(variable_name="chat_history"),
        ("human", "{input}"),
        MessagesPlaceholder(variable_name="agent_scratchpad"),
    ])

    agent = create_openai_tools_agent(llm, tools, prompt)
    return AgentExecutor(agent=agent, tools=tools, verbose=True)

def run_interview():
    """运行面试流程"""
    # 初始化会话
    session = InterviewSession(
        session_id="S-20260116-001",
        candidate=SAMPLE_CANDIDATE
    )

    # 创建上下文组装器
    assembler = ContextAssembler(session)

    # 创建 Agent
    agent = create_interview_agent()

    # 对话历史(短期记忆)
    chat_history = []

    print("=" * 60)
    print("AI 面试官已就绪,输入 'quit' 退出,输入 'next' 进入下一阶段")
    print("=" * 60)

    phases = ["opening", "technical", "closing"]
    phase_idx = 0

    while True:
        session.current_phase = phases[phase_idx]
        system_prompt = assembler.assemble()

        user_input = input("\n👤 候选人: ").strip()

        if user_input.lower() == "quit":
            print("\n面试结束,感谢参与!")
            break

        if user_input.lower() == "next":
            phase_idx = min(phase_idx + 1, len(phases) - 1)
            print(f"\n[系统] 进入 {phases[phase_idx]} 阶段")
            continue

        # 调用 Agent
        result = agent.invoke({
            "system_prompt": system_prompt,
            "chat_history": chat_history,
            "input": user_input
        })

        response = result["output"]
        print(f"\n🤖 面试官: {response}")

        # 更新对话历史
        chat_history.append(HumanMessage(content=user_input))
        chat_history.append(AIMessage(content=response))

        # 简单模拟记录问答
        session.questions_asked.append({
            "question": "从对话中提取",
            "answer": user_input
        })

if __name__ == "__main__":
    # 确保设置了 OPENAI_API_KEY
    if not os.getenv("OPENAI_API_KEY"):
        print("请设置 OPENAI_API_KEY 环境变量")
    else:
        run_interview()

代码亮点

  1. Tools 定义清晰:每个工具都有明确的 docstring,让 LLM 知道什么时候该调用。
  2. Memory 分层Candidate 是长期记忆(跨面试),InterviewSession 是短期记忆(本场面试)。
  3. 动态 PromptContextAssembler 根据面试阶段(opening/technical/closing)组装不同的 System Prompt。
  4. 滑动窗口get_interview_history() 只取最近 5 条记录,防止 Context Overflow。

扩展阅读


本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。