芥末
发布于 2026-01-07 / 0 阅读
0
0

Agent 上下文工程实战:从提示词、状态管理到 MCP

大语言模型(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 完成一次代码迁移:

  1. 扫描项目结构;
  2. 找出旧 API;
  3. 修改代码;
  4. 执行测试;
  5. 修复失败用例;
  6. 生成迁移报告。

提示词可以告诉模型“你要谨慎修改代码”,但它无法天然知道当前已经走到第几步。如果历史记录里散落着工具输出、错误日志、用户补充说明和模型回复,模型就要从大量文本中重新推断状态,稳定性会变差。

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。模型不用在几千行历史中查找“做到哪了”,而是直接基于明确状态做下一步决策。

状态显性化还便于程序控制。系统可以检查 phasependingconstraints,决定是否允许某个工具调用,或者在高风险操作前要求用户确认。

工具调用:让模型具备行动能力

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 服务、数据库服务、浏览器服务
ClientHost 内负责连接 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 系统中的一层约束。真正决定长期稳定性的,是上下文能不能持续、清晰、低噪声地表达当前任务状态。


评论