
03-上下文工程Agent的“眼睛”消息四种角色与核心循环代码实现上一篇我们聊了 Harness 工程与 ReAct 循环这一篇把镜头怼到 Agent 的“眼睛”上——上下文Context。很多人以为 Agent 的能力上限由模型参数量决定参数越大越聪明。这个说法只对了一半同一个模型喂给它的上下文不一样表现可以天差地别。李博杰在书里反复强调上下文决定 Agent 能力的上限模型只决定下限。模型是大脑上下文是眼睛——眼睛不行大脑再好也是睁眼瞎。一、上下文为什么决定上限先建立一个直觉LLM 是无状态的。它不记得你是谁、昨天聊了什么、刚才调用了什么工具。每一次调用 API模型都是从零开始“现场阅读”你给它的全部信息然后输出。这个“全部信息”就是上下文。也就是说Agent 的每一次“思考”都是基于当前上下文的一次重新理解。上下文里没有的信息模型不知道上下文里被污染的信息比如塞了一堆无关日志会稀释模型的注意力。这就好比你去医院看病医生只能看你递过去的病历本——病历写得清楚诊断就准病历里夹了三张外卖小票医生再厉害也得皱眉。所以“上下文工程”Context Engineering的本质就一句话在有限的窗口里把对的信息在对的时机放进去。二、API 消息的四种角色打开任何一家大模型 API 文档OpenAI、Anthropic、DeepSeek 都差不多你会发现对话是用一个 messages 列表表示的每条消息有个 role 字段只有四种取值。这四种角色就是上下文的基本积木角色谁在说话作用类比system开发者设定身份、规则、约束全局有效员工手册user终端用户提出请求、补充信息客户开口assistant模型模型的回复包括文字和工具调用请求员工答复tool工具执行器把工具执行结果回填给模型查完系统的回执几个新手容易踩的点system消息一般放最前面且全局只有一条或少数几条它是你作为开发者能施加的最强控制手段。assistant消息不一定是文字。当模型决定调用工具时它输出的是一个结构化的tool_calls字段里面带函数名和 JSON 参数——这也算一条assistant消息。tool消息必须紧跟在带tool_calls的assistant消息后面相当于“回执”。没有回执模型会认为工具还没执行完。三、从单轮对话到 Agent 核心循环最简单的调用只需要 system user 两条消息模型返回一段文字结束。这是“聊天机器人”不是 Agent。Agent 的分水岭在于模型返回的不是文字而是工具调用请求。你的代码负责真正执行工具把结果以tool消息塞回去模型再看结果决定下一步——是继续调工具还是给出最终答复。这个“请求→执行→回填→再看”的过程循环往复就是上一篇讲的 ReAct 循环在 API 层面的真实形态。一句话总结带工具调用的多轮交互 Agent 核心循环。所谓 Agent 框架LangChain、OpenAI Agents SDK 等剥掉包装内核都是这一段循环。四、用 Python 实现核心循环下面用伪 OpenAI 风格的接口写一个最小可用的 Agent 循环场景是无人售货柜客服用户投诉“扣了两次钱”Agent 先查支付记录确认重复扣款后调用退款工具。importjson# 1. 定义工具暴露给模型的说明书tools[{type:function,function:{name:query_payment_records,description:根据订单号查询支付流水返回所有扣款与冲正记录,parameters:{type:object,properties:{order_id:{type:string,description:订单号}},required:[order_id],},},},{type:function,function:{name:create_refund,description:对指定扣款流水发起退款金额单位为分,parameters:{type:object,properties:{payment_id:{type:string},amount:{type:integer},},required:[payment_id,amount],},},},]# 2. 本地真正执行工具的函数生产环境里对接 SpringBoot 微服务defexecute_tool(name,args):ifnamequery_payment_records:# 模拟查库两笔扣款第二笔未冲正returnjson.dumps({order_id:args[order_id],records:[{payment_id:P-1001,amount:500,status:SUCCESS},{payment_id:P-1002,amount:500,status:SUCCESS_NO_REVERSE},],},ensure_asciiFalse)ifnamecreate_refund:returnjson.dumps({refund_id:RD-8848,status:CREATED},ensure_asciiFalse)returnjson.dumps({error:funknown tool{name}})# 3. 核心循环整个 Agent 的心脏不到 30 行messages[{role:system,content:你是无人售货柜客服处理扣款异常时必须先查支付流水再操作退款。},{role:user,content:我在 3 号柜买了瓶可乐被扣了两次钱订单号 20260818001快点退我},]whileTrue:respllm.chat(messagesmessages,toolstools)# 一次模型调用msgresp.choices[0].messageifmsg.tool_calls:# 模型想动手messages.append(msg)# 记住它的请求forcallinmsg.tool_calls:argsjson.loads(call.function.arguments)resultexecute_tool(call.function.name,args)messages.append({# 回填观察role:tool,tool_call_id:call.id,content:result,})continue# 带着观察进入下一轮思考print(msg.content)# 没有工具调用 最终答复break盯着messages这个列表看它随着循环不断变长模型每一轮“思考”时看到的东西都不一样。这个列表就是上下文的全部实体。所谓上下文工程就是管理这个列表的艺术——往里加什么、什么时候裁剪、按什么顺序排。五、上下文的构成四要素拆解把上面代码里的 messages 展开一个成熟 Agent 的上下文由四部分构成系统提示身份、规则、红线。示例中那句“必须先查流水再退款”就是护栏。工具定义tools 列表也会被序列化进上下文。工具描述写得好不好直接决定模型会不会用、会不会滥用。对话历史user 与 assistant 的交替记录是模型的“短期记忆”。观察结果tool 消息回填的真实世界数据。注意这部分的量往往最大——一条支付流水、一段日志、一张 YOLO 检测结果 JSON都可能很长是上下文膨胀的头号元凶。六、一个重要澄清上下文 ≠ 环境最后纠正一个常见误解上下文不是环境本身上下文是环境在 Agent 内部的表示。真实环境里无人售货柜有温度、有货道余量、有摄像头画面信息是连续且无穷的而上下文是离散的、有限的文本快照。Harness 的职责之一就是决定“环境的哪些部分、以什么粒度、什么格式”进入这个快照。快照拍得好模型如临现场拍得差模型等于盲人摸象。理解了这层关系你也就理解了为什么说“观察空间的设计是提升 Agent 表现的主要工程手段”——设计观察空间本质上就是在设计上下文的取景框。小结上下文决定能力上限模型决定下限上下文工程 在有限窗口里放对的信息。四种消息角色system/user/assistant/tool是上下文的基本积木tool_calls与tool回执构成工具闭环。Agent 核心循环在代码层面就是一个 while 循环 一个不断增长的 messages 列表任何框架的内核不过如此。上下文四要素系统提示、工具定义、对话历史、观察结果其中观察结果最容易失控。上下文是环境在 Agent 内部的表示而非环境本身——设计观察空间就是在设计取景框。