大语言模型(Large Language Model,LLM)刚进入应用开发时,最常见的产品形态是 Chatbot。Chatbot 的工作方式很直接:用户发来一句话,模型根据当前输入和少量历史对话生成回答,交互基本围绕“问答”展开。
这种模式对上下文的要求不高。只要用户问题清楚,系统提示词能约束角色、语气和输出格式,模型通常就能给出可用结果。
Agent 的工作方式完全不同。Agent 不是回答一次问题就结束,而是围绕一个目标持续推进任务。它需要拆解步骤、调用工具、观察结果、修正计划,并在多轮循环中保持任务方向不变。
一个普通 Chatbot 的调用链路大致是这样:
flowchart LR
U[用户输入] --> P[提示词与少量历史]
P --> L[LLM 生成回答]
L --> R[返回用户]
Agent 则更像一个带状态的执行系统:
flowchart TD
G[用户目标] --> C[上下文管理器]
C --> L[LLM 决策]
L --> A{选择动作}
A -->|直接回答| R[输出结果]
A -->|调用工具| T[外部工具/API/人类确认]
T --> O[观察结果]
O --> S[更新任务状态]
S --> C
差异的核心不在“模型会不会说话”,而在模型每一轮决策时能不能准确知道:
- 当前目标是什么;
- 已经完成了哪些步骤;
- 哪些信息来自用户,哪些信息来自工具;
- 哪些结论已经确认,哪些只是猜测;
- 哪些动作可以继续做,哪些动作需要审批;
- 距离任务完成还差什么。
这些内容都要通过上下文呈现给模型。上下文不再只是补充背景,而是 Agent 的运行状态载体。
提示词工程能解决什么,不能解决什么
提示词工程仍然有价值。它适合解决三类问题:
| 能力 | 典型写法 | 适合程度 |
|---|---|---|
| 角色设定 | “你是一个 SQL 优化助手” | 很适合 |
| 行为边界 | “不要修改生产数据,只生成建议” | 很适合 |
| 输出格式 | “以 JSON 返回,字段包含 reason 和 action” | 很适合 |
但提示词本质上是静态规则。它擅长描述“不变的约束”,不擅长维护“持续变化的状态”。
当任务变成多轮执行时,提示词会遇到几个明显边界。
1. 静态提示词无法表达动态进度
例如用户要求 Agent 完成一次代码迁移:
- 扫描项目结构;
- 找出旧 API;
- 修改代码;
- 执行测试;
- 修复失败用例;
- 生成迁移报告。
提示词可以告诉模型“你要谨慎修改代码”,但它无法天然知道当前已经走到第几步。如果历史记录里散落着工具输出、错误日志、用户补充说明和模型回复,模型就要从大量文本中重新推断状态,稳定性会变差。
2. 上下文越长,提示词越容易被稀释
很多系统会把更多信息塞进上下文,以为模型看到得越多,回答越准。实际情况不是这样。
上下文窗口虽然变大了,但模型注意力不是无限的。无关内容、重复内容、过期内容都会和关键内容竞争关注度。提示词放在开头并不意味着它永远有足够控制力,尤其是在长任务中,大量新信息会不断改变模型的判断依据。
3. 提示词对模型版本和参数敏感
同一段提示词,在不同模型、不同温度参数、不同工具调用实现下,可能表现不同。短期调一段提示词能解决问题,但长期维护一个 Agent 系统,不能把稳定性完全押在一段自然语言提示上。
更可靠的做法是把提示词降级为系统的一部分,让上下文结构、状态管理、工具协议和运行监控共同约束 Agent 行为。
上下文工程到底是什么
上下文工程不是“把资料拼进 prompt”。更准确地说,它是在每一次模型调用前,决定模型应该看到什么、以什么顺序看到、用什么结构看到,以及哪些内容应该被压缩、删除或长期保存。
可以把上下文工程拆成五个动作:
flowchart LR
A[收集信息] --> B[分类信息]
B --> C[选择高价值内容]
C --> D[结构化组装]
D --> E[调用模型]
E --> F[写回状态]
F --> C
一个 Agent 的上下文通常包含这些信息:
| 上下文类型 | 作用 | 示例 |
|---|---|---|
| 系统规则 | 定义模型行为边界 | 不能执行高风险操作,必须解释工具调用原因 |
| 用户目标 | 定义任务终点 | “把项目从 Vue2 迁移到 Vue3” |
| 当前状态 | 表达任务进度 | 已完成扫描,正在修改组件 API |
| 短期历史 | 保留最近交互 | 最近几轮对话、最近一次工具结果 |
| 长期记忆 | 复用稳定事实 | 用户偏好、项目规范、历史决策 |
| 工具描述 | 告诉模型能做什么 | search_files、run_tests、ask_user |
| 工具结果 | 反馈外部世界状态 | 测试失败日志、API 返回值 |
| 约束条件 | 限制行动范围 | 不能删除文件,修改前必须备份 |
| 输出契约 | 约束返回结构 | 必须返回 action、reason、state_delta |
上下文工程关注的不是单句提示是否“聪明”,而是整个信息系统是否清晰、稳定、可维护。
上下文不是越多越好
Agent 开发里很常见的错误,是把所有历史、所有工具返回、所有文档片段都塞进上下文。这样做会带来四类问题。
| 问题 | 具体表现 | 后果 |
|---|---|---|
| 信息噪声 | 大量内容和当前决策无关 | 模型抓不住重点 |
| 状态冲突 | 旧结论和新结论同时存在 | 模型使用过期信息 |
| 成本上升 | token 数量持续膨胀 | 延迟和费用增加 |
| 行为漂移 | 上下文结构每轮都不同 | 输出风格和决策不稳定 |
更好的原则是:每轮只给模型当前决策所需的信息,同时保留必要的状态摘要。
可以把上下文分成不同生命周期:
| 层级 | 生命周期 | 存放内容 | 管理方式 |
|---|---|---|---|
| 即时上下文 | 当前一次调用 | 当前问题、最近工具结果 | 每轮重建 |
| 工作记忆 | 当前任务期间 | 计划、进度、待办、已确认事实 | 持续更新 |
| 长期记忆 | 跨任务复用 | 用户偏好、业务规则、项目背景 | 检索后注入 |
| 外部存储 | 长期保存 | 文件、日志、报告、向量索引 | 按需读取 |
这个分层能避免一个问题:模型不需要每次都看到全部历史,只需要看到经过整理的任务快照。
状态显性化:Agent 稳定运行的关键
如果任务状态只隐藏在对话历史里,模型每轮都要重新“猜”当前进展。状态显性化就是把这些隐含信息整理成结构化数据,让模型一眼知道任务处于什么阶段。
一个可用的任务状态可以长这样:
task:
goal: "将项目中的旧鉴权接口迁移到新版 SDK"
phase: "修改代码"
status: "in_progress"
progress:
completed:
- "已扫描 src/auth 目录"
- "已定位 6 处旧接口调用"
- "已完成 login.ts 的迁移"
current_step: "迁移 token-refresh.ts"
pending:
- "迁移 logout.ts"
- "运行单元测试"
- "生成变更报告"
facts:
confirmed:
- "新版 SDK 的 refreshToken 方法返回 Promise<AuthResult>"
- "旧接口 refresh_token 已弃用"
assumptions:
- "logout.ts 使用同一套鉴权封装"
constraints:
- "不能修改 public API"
- "修改文件前需要记录 diff"
- "测试失败时不得继续批量修改"
open_questions:
- "新版 SDK 是否需要兼容旧 token 格式?"
这种状态快照比完整聊天记录更适合 Agent。模型不用在几千行历史中查找“做到哪了”,而是直接基于明确状态做下一步决策。
状态显性化还便于程序控制。系统可以检查 phase、pending、constraints,决定是否允许某个工具调用,或者在高风险操作前要求用户确认。
工具调用:让模型具备行动能力
Agent 和 Chatbot 的另一大差异,是 Agent 能调用工具。工具可以是搜索接口、数据库查询、代码执行器、浏览器、文件系统、第三方 API,也可以是人类确认。
从模型视角看,只要某个外部对象能接收输入、返回结果,并影响后续决策,它就可以被抽象成工具。
工具定义通常包含四部分:
| 字段 | 作用 |
|---|---|
| name | 工具名称,模型用它选择动作 |
| description | 工具适用场景,帮助模型判断何时调用 |
| parameters | 输入参数结构,减少调用歧义 |
| result schema | 返回结果结构,便于写回上下文 |
例如,一个 Agent 可以把“任务完成”和“向用户澄清问题”也设计成工具:
const taskCompletionTool = {
type: "function",
function: {
name: "task_complete",
description: "当用户任务已经完成时调用,用于结束当前 Agent 循环",
parameters: {
type: "object",
properties: {}
}
}
};
const askQuestionTool = {
type: "function",
function: {
name: "ask_question",
description: "当任务存在关键信息缺口,需要用户补充或确认时调用",
parameters: {
type: "object",
properties: {
question: {
type: "string",
description: "需要用户回答的问题"
},
reason: {
type: "string",
description: "为什么必须先澄清这个问题"
}
},
required: ["question", "reason"]
}
}
};
const exitLoopTools = [taskCompletionTool, askQuestionTool];
这种设计有一个好处:Agent 的退出、暂停、澄清都变成了明确动作,而不是靠模型自由发挥一句“我完成了”或“我不确定”。
工具结果不能直接乱塞进上下文
工具调用后,系统必须把结果反馈给模型。但反馈方式很关键。
错误做法是把原始日志、完整 JSON、长网页内容直接追加到上下文末尾。模型看到的内容多了,却不一定更容易判断下一步。
更稳的做法是把工具结果写成结构化观察记录:
{
"tool_call": {
"name": "run_tests",
"arguments": {
"command": "npm test -- auth"
}
},
"observation": {
"status": "failed",
"summary": "auth 模块 2 个测试失败",
"key_findings": [
"refreshToken should return AuthResult 测试失败",
"logout should clear local token 测试失败"
],
"artifacts": [
{
"type": "log",
"path": "artifacts/test-auth-20260607.log"
}
]
},
"state_delta": {
"phase": "debugging",
"pending": [
"修复 refreshToken 返回值不兼容问题",
"检查 logout 清理逻辑"
],
"blocked": false
}
}
这里有几个设计点:
summary给模型快速理解结果;key_findings保留关键证据;artifacts指向外部大文件,避免日志塞满上下文;state_delta明确说明这次工具调用如何改变任务状态。
工具结果写回上下文时,应该优先保留“会影响下一步决策的信息”,而不是保留所有过程数据。
思考过程不等于完整推理日志
复杂任务中,模型会产生中间判断。中间判断对后续决策有用,但不代表要把所有推理痕迹完整保存。
更合适的做法是保存“决策摘要”,而不是保存冗长的逐步推理。上下文中需要的是可复用的结论、假设、证据和待验证事项。
| 内容 | 是否适合长期进入上下文 | 原因 |
|---|---|---|
| 已确认事实 | 适合 | 后续决策需要引用 |
| 用户明确要求 | 适合 | 属于强约束 |
| 工具关键结果 | 适合 | 反映外部真实状态 |
| 冗长推理过程 | 通常不适合 | 占用上下文,可能干扰判断 |
| 临时猜测 | 谨慎保留 | 必须标注为 assumption |
| 错误尝试 | 可摘要保留 | 防止重复犯错 |
例如,可以这样记录一次决策:
decision:
topic: "refreshToken 返回值适配方式"
conclusion: "在调用层增加 adapter,不直接修改新版 SDK 返回结构"
evidence:
- "新版 SDK 返回 Promise<AuthResult>"
- "业务代码仍依赖旧字段 access_token"
rejected_options:
- option: "修改 SDK 类型定义"
reason: "会影响其他模块,风险过高"
follow_up:
- "为 adapter 增加单元测试"
这种记录方式既保留了关键判断,又不会让上下文变成不可维护的推理堆栈。
MCP:把上下文与工具接入标准化
MCP(Model Context Protocol,模型上下文协议)可以理解为一种把模型应用和外部能力连接起来的标准化方式。它解决的不是“模型会不会调用工具”,而是“工具、资源、提示模板和上下文信息如何以统一结构暴露给模型应用”。
在没有协议约束时,Agent 系统往往会变成这样:
flowchart TD
A[Agent 主程序] --> B[搜索 API 拼接代码]
A --> C[数据库查询拼接代码]
A --> D[文件系统拼接代码]
A --> E[业务系统拼接代码]
B --> F[自由文本上下文]
C --> F
D --> F
E --> F
每接一个新工具,都要写一套私有适配逻辑;每种工具返回结构都不同;上下文里混着状态、日志、工具描述和业务数据。系统越复杂,越难排查模型为什么做出某个决策。
引入 MCP 后,可以把外部能力整理成统一接口:
flowchart LR
H[Agent Host<br/>应用/IDE/平台] --> C1[MCP Client]
C1 --> S1[MCP Server<br/>代码仓库]
C1 --> S2[MCP Server<br/>数据库]
C1 --> S3[MCP Server<br/>浏览器]
C1 --> S4[MCP Server<br/>业务系统]
S1 --> R1[Resources]
S2 --> T1[Tools]
S3 --> T2[Tools]
S4 --> P1[Prompts]
MCP 常见抽象包括:
| 抽象 | 含义 | 例子 |
|---|---|---|
| Tools | 可执行动作 | 查询数据库、运行测试、创建工单 |
| Resources | 可读取资源 | 文件内容、配置、日志、文档 |
| Prompts | 可复用提示模板 | 代码审查模板、故障排查模板 |
| Server | 某类外部能力的提供方 | Git 服务、数据库服务、浏览器服务 |
| Client | Host 内负责连接 Server 的组件 | IDE 插件、Agent 运行时 |
MCP 对上下文工程的价值在于:它让外部信息以稳定结构进入 Agent 系统,而不是靠临时拼接文本。
工具列表也需要上下文管理
工具越多,模型越难选。把所有工具一次性暴露给模型,会让选择空间变大,也会增加误调用概率。
常见做法有三种:
| 方案 | 做法 | 适合场景 | 风险 |
|---|---|---|---|
| 全量暴露 | 每轮都给模型全部工具 | 工具少、任务简单 | 工具多时干扰严重 |
| 动态裁剪 | 根据阶段只给相关工具 | 多阶段任务 | 工具突然消失,模型认知可能不稳定 |
| 显式屏蔽 | 保留工具认知,但标注当前不可用或低优先级 | 长任务、复杂 Agent | 上下文略长 |
在一些长周期 Agent 中,与其把暂时不用的工具完全移除,不如把它标注为“当前阶段不可调用”或“仅在满足条件时调用”。这样模型能保持稳定的能力地图,不会因为工具列表频繁变化而误判系统边界。
示例:
available_tools:
- name: "search_code"
status: "enabled"
use_when: "需要定位代码引用或函数定义"
- name: "edit_file"
status: "enabled"
use_when: "已经确认修改范围,并且修改不涉及高风险文件"
- name: "deploy_prod"
status: "disabled"
reason: "当前任务处于开发环境验证阶段,生产部署需要用户批准"
- name: "ask_user"
status: "enabled"
use_when: "缺少关键业务规则或需要高风险操作确认"
这属于上下文工程的一部分:模型不仅要知道“有什么工具”,还要知道“当前能不能用、什么时候用、为什么不能用”。
Agent 可靠性:不能只靠模型自觉
Agent 一旦能调用工具,就有了真实副作用。它可能修改文件、访问数据库、创建任务、发送消息,甚至触发线上操作。可靠性必须从系统层设计,而不是期待模型每次都谨慎。
一个可靠的 Agent 运行时至少需要这些能力:
flowchart TD
A[用户目标] --> B[任务图/状态机]
B --> C[LLM 决策节点]
C --> D{工具调用是否允许}
D -->|否| E[拒绝/要求确认]
D -->|是| F[执行工具]
F --> G{执行结果}
G -->|成功| H[更新状态]
G -->|失败| I[错误处理/重试/回滚]
H --> J[记录 Trace]
I --> J
J --> K[评估与监控]
K --> B
可靠执行
可靠执行关注 Agent 循环能不能稳定推进。工程上通常需要:
- 状态机或任务图,避免流程完全靠模型自由跳转;
- checkpoint,任务中断后可以恢复;
- 超时控制,防止工具调用卡死;
- 重试策略,处理临时网络或服务错误;
- 幂等设计,避免重复执行造成副作用;
- 高风险动作审批,例如删除文件、写数据库、部署生产环境。
LangGraph 这类框架适合把 Agent 流程建模成图。每个节点负责一类操作,边表示状态转移,系统可以明确控制何时调用模型、何时调用工具、何时进入人工确认。
错误处理
工具失败不应该只把错误日志塞给模型。更稳的错误记录应包含:
{
"error": {
"tool": "edit_file",
"type": "permission_denied",
"message": "目标文件只读,无法写入",
"retryable": false
},
"impact": {
"current_step": "修改 token-refresh.ts",
"blocked": true
},
"suggested_next_actions": [
"请求用户授予写权限",
"生成补丁文件而不是直接写入"
]
}
这样模型能明确知道:错误是否可重试、当前任务是否阻塞、下一步有哪些安全选项。
可观测性
Agent 的问题经常不是“最终答案错了”这么简单,而是中间某一步工具选错、上下文缺失、状态过期或约束没有生效。没有可观测性,很难定位原因。
需要记录的关键 Trace 包括:
| Trace 内容 | 用途 |
|---|---|
| 每轮输入上下文快照 | 排查模型看到的信息是否正确 |
| 模型输出 | 分析决策依据和格式问题 |
| 工具调用参数 | 检查是否传错参数 |
| 工具返回结果 | 判断外部反馈是否被正确解析 |
| 状态变更 | 追踪任务进度是否被正确更新 |
| token、延迟、费用 | 控制运行成本 |
| 错误与重试记录 | 优化稳定性策略 |
LangSmith 可以用于 AI 应用的追踪、调试、评估和监控。LangGraph Studio 则更偏向 Agent 图的可视化调试,可以观察节点执行、状态流转和工具调用结果。
评估
Agent 评估不能只看单轮回答质量,还要看完整任务是否被正确完成。
评估维度可以这样设计:
| 维度 | 评价问题 |
|---|---|
| 任务完成度 | 是否达成用户目标 |
| 步骤正确性 | 是否按合理顺序推进 |
| 工具使用 | 是否调用了合适工具,参数是否正确 |
| 状态一致性 | 是否重复执行、遗漏步骤或使用旧信息 |
| 安全性 | 是否越权、是否执行高风险操作 |
| 成本 | token、工具调用次数、耗时是否可接受 |
| 可解释性 | 是否能追溯关键决策 |
评估方式可以结合人工标注和 LLM-as-a-Judge(使用大语言模型作为评审器)。原型阶段可以先准备一批典型任务,让人工标注期望行为;任务变多后,再用评审模型辅助扩充测试集和回归测试。
一个可落地的上下文组装流程
工程实现时,可以把上下文组装做成独立模块,而不是散落在业务代码里。
伪代码大致如下:
def build_context(task_id: str, user_input: str) -> list[dict]:
task_state = load_task_state(task_id)
recent_events = load_recent_events(task_id, limit=8)
relevant_memory = retrieve_memory(
query=user_input,
filters={"project": task_state.project}
)
enabled_tools = select_tools(
phase=task_state.phase,
permissions=task_state.permissions
)
context = [
{
"role": "system",
"content": render_system_rules()
},
{
"role": "developer",
"content": render_constraints(task_state.constraints)
},
{
"role": "user",
"content": user_input
},
{
"role": "context",
"content": render_task_snapshot(task_state)
},
{
"role": "context",
"content": render_memory(relevant_memory)
},
{
"role": "context",
"content": render_recent_events(recent_events)
},
{
"role": "context",
"content": render_tool_policy(enabled_tools)
}
]
return trim_context(context, budget=task_state.token_budget)
这个流程里有几个关键点:
load_task_state获取的是显性状态,不是完整聊天记录;retrieve_memory只检索和当前输入相关的长期记忆;select_tools根据任务阶段选择工具;trim_context按预算裁剪上下文;- 每类信息都有固定渲染格式,避免每轮上下文结构漂移。
对应的写回流程也要独立:
def apply_model_result(task_id: str, model_result: dict) -> None:
if "tool_call" in model_result:
validate_tool_call(model_result["tool_call"])
observation = execute_tool(model_result["tool_call"])
state_delta = extract_state_delta(observation)
append_event(task_id, observation)
update_task_state(task_id, state_delta)
elif "final_answer" in model_result:
append_event(task_id, {
"type": "final_answer",
"content": model_result["final_answer"]
})
mark_task_done(task_id)
else:
append_event(task_id, {
"type": "invalid_model_result",
"raw": model_result
})
Agent 的稳定性来自闭环:上下文构建、模型决策、工具执行、结果写回、状态更新,每一步都要有结构。
常见坑和处理办法
| 坑 | 表现 | 处理办法 |
|---|---|---|
| 把全部历史塞给模型 | token 暴涨,模型忽略关键状态 | 用任务快照替代完整历史 |
| 工具结果太原始 | 日志、网页、JSON 混在上下文里 | 提取 summary、key_findings、state_delta |
| 状态没有版本 | 新旧结论冲突 | 给事实和决策加时间、来源、状态 |
| 工具描述含糊 | 模型误选工具 | 写清 use_when、参数、限制和副作用 |
| 高风险动作无审批 | Agent 可能误删、误改、误发 | 引入权限、审批和 dry-run |
| 长期记忆污染 | 错误偏好被反复注入 | 记忆需要可撤销、可过期、可追溯 |
| 只评估最终回答 | 中间步骤错误难发现 | 记录 Trace,评估工具调用和状态变化 |
| 频繁改变上下文格式 | 模型行为不稳定 | 固定上下文模板和字段语义 |
上下文工程的核心判断
Agent 的能力不是单靠更长提示词堆出来的。模型每一轮决策都依赖它看到的信息,如果上下文里充满噪声、冲突和过期状态,模型再强也容易跑偏。
更可靠的 Agent 系统会把上下文当作工程资源来管理:
- 用显性状态替代零散历史;
- 用结构化工具结果替代原始输出堆叠;
- 用分层记忆区分短期任务和长期知识;
- 用 MCP 这类协议标准化外部能力接入;
- 用状态机、Trace、评估和监控兜住可靠性。
提示词仍然重要,但它只是 Agent 系统中的一层约束。真正决定长期稳定性的,是上下文能不能持续、清晰、低噪声地表达当前任务状态。