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),怎么压缩?
用一张图说清楚:
三、实战:一个"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 里的那些雷
-
上下文中毒 (Context Poisoning): 别以为喂得越多越好。塞进去一堆无关信息(Context Confusion),AI 会晕的。记住:"Less is More",精准检索 > 海量检索。
反面案例:把候选人所有历史面试记录(包括 5 年前的)都塞进去,结果 AI 说"根据你 2019 年的面试表现..."——尴尬。
-
工具爆炸 (Tool Explosion): 别给 Agent 一百个工具。它会不知道选哪个。如果你有很多工具,请用 RAG 的方式去检索工具描述,动态挂载最相关的几个。
-
格式即正义 (Format Matters): 给 AI 看的资料,最好是结构化的(Markdown, JSON)。别给它看 OCR 扫描出来的乱码 PDF。
-
上下文窗口溢出 (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()
代码亮点
- Tools 定义清晰:每个工具都有明确的 docstring,让 LLM 知道什么时候该调用。
- Memory 分层:
Candidate是长期记忆(跨面试),InterviewSession是短期记忆(本场面试)。 - 动态 Prompt:
ContextAssembler根据面试阶段(opening/technical/closing)组装不同的 System Prompt。 - 滑动窗口:
get_interview_history()只取最近 5 条记录,防止 Context Overflow。
扩展阅读
- Elastic Blog: What is context engineering?
- How to Fix Your Context
- 12-Factor Agents - Principles for building reliable LLM applications
- LangGraph Memory Management
- Don't Build Multi-Agents
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
