对话历史作为核心的上下文信息,为LLM建立起对话的语境,是决定LLM响应内容的关键输入之一。对话历史中的每一条消息都绑定这一个固定的角色,典型的角色包括System、User、Assistant和Tool。但是这并不意味着每条消息一定要求都要由对应的角色生成,为了提供LLM推理的质量,用户和工具都能根据需求向对话历史注入消息,而且这些消息可以具有任意的角色,前提是消息的结构符合要求。
1. 为什么需要消息注入
对于采用ReAct循环构建的Agent来说,如果不使用Session/Thread,如下的流程体现了对话历史常规的构建模式:
- 最终作为对话历史的消息列表为空;
- 用户调用Agent的请求被转换成一条
User消息被添加到消息列表中;Agent调用也可以同时指定System消息; - 整个对话历史作为输入的一部分提交给LLM,后者创建一个
Assistant消息作为响应被添加到消息列表中; - 如果回复的
Assistant消息携带工具调用,Agent会调用对应的工具,并将每个工具调用生成的结果封装成一个Tool消息被添加到消息列表中; - 整个整个对话历史作为输入的一部分在此提交给LLM,后者基于新的对话历史生成新的
Assistant消息作为响应被添加到消息列表中; - 以此类推,形成一个循环。
如果进一步采用Session/Thread,隶属同一Session的所有消息将会添加到同一个对话历史中,对话历史构建的语境是Agent与LLM进行多轮对话的基础,因为LLM调用是完全无状态的。
在这个典型的ReAct循环中,消息的角色完全与消息的来源是完全一致的的,User和System消息由用户输入生成,Assistant消息由LLM生成,Tool消息由工具调用生成,System消息由Agent调用时指定生成。但这并非硬性规定,对话历史仅仅是提供给LLM的上下文信息而已,为了让LLM的响应更符合预期,不仅用户在调用Agent时可以输入一组具有任意角色的消息来模拟一段对话,工具调用也能够在必需的Tool消息之后注入任意的具有有效结构的多条消息。这本质上也是一种典型的提示词工程的手段,典型的应用场景包括:
1.1 复杂工具调用的少样本示范
当工具的参数极其复杂(如需要传入特定的JSON结构或SQL 语句),或者需要多工具组合调用时,仅靠系统提示词的文字说明,模型很容易出错。此时可以人工编造1-2轮用户提问 ➔ 模型思考 ➔ 工具返回数据 ➔ 模型最终回答的完整历史,比如:
- 复杂数据库查询:构建一段历史,展示用户查询“上季度销售额前三的员工”,模型调用
execute_sql工具,工具返回一串复杂的JSON数组,模型再将其翻译成话术。让LLM学会如何正确处理工具返回的结构化数据; - 多接口级联调用:编造一段历史,演示模型先调用
get_user_id,拿到ID后再自动调用get_user_order的过程。模型看到这段历史,就知道自己也可以连续触发工具。
1.2 引导模型进行反思与纠错的预热
如果想让Agent具备极强的抗挫折能力(即工具报错时不要懵掉,而是自动换个参数重试),最好的方式就是喂给它一段失败成功史,比如:
- API权限/参数错误处理:历史中,用户让Agent “查一下明天的天气”。模型第一次调用工具传入了旧版参数,
Tool消息返回{"error": "Invalid date format, use YYYY-MM-DD"}。接着构建模型在历史中自我纠错:哦,格式错了,我需要重新调用,并传入正确参数; - 代码执行器纠错:在CodeAct场景中,预先注入一段代码运行抛出
IndexError的历史,以及模型随后如何修改索引并重新运行成功的历史。这样模型在实际遇到报错时,会模仿历史中的自己去修复Bug。
1.3 规范模型的思考链与内部对白
在Agent中,我们希望模型在调用工具前先写下思考过程,但模型经常会忘记写或者写得很敷衍。此时我们可以构建一段包含高度结构化Assistant思考格式和Tool交互的历史。比如在一个要求极度严谨的财务/医疗推理的Agent中,构建一段历史展现模型在收到用户指令后,先在思考(Thought)中拆解了三个步骤,然后调用ToolA,收到结果后又写了一段对思考的修正计划,再调用ToolB。这相当于给模型立了一个严谨工作的榜样,迫使它在当下的推理中也采用这种深思熟虑的模式。
1.4 模拟历史场景进行情景剧重现
有时候我们需要Agent在一个特定的、预设的业务进度中清醒过来并继续工作,而不是从零开始聊天。此时需要生成一段多轮对话和工具调用记录,作为该任务的前情提要。比如:
- 断点续传/任务接管:一个复杂的自动化分析任务执行到一半中断了。系统在重启任务时,把之前已经发生的对话、已经调用过的工具和拿到的数据,全部打包成一段“历史”喂给新的LLM实例。LLM看到这段构建的历史,就能立刻无缝接管上一个实例的工作进度;
- 反欺诈/安全演练沙箱:为了测试或训练某个防御性 Agent,系统故意构建一段“黑客(User)正在尝试通过钓鱼工具(Tool)套取信息”的历史,然后让Agent接手最后一轮,看它是否能识别出风险并拒绝。
1.5 冷启动与复杂状态初始化
在用户刚打开软件,还没开始真正聊天时,系统为了给模型注入一些心智背景或前置技能,会在首轮对话前,注入几条用户和工具的互动线索。比如在一个作为私人助理Agent初始化时构建几条伪造的记录。虽然用户没说过这话,但LLM读到这段历史后,接下来的所有回复都会自动遵循极简风的设定。
- User:导入我的个人喜好;
- Tool:该用户喜欢极简风、讨厌长篇大论。
由于Agent调用自身就支持针对消息列表的输出,所有我们主要关注工具调用后注入消息的场景,接下来我们通过一个简单的例子演示一下基于工具调用的消息注入在MAF和LangChain中该如何实现。
2. 在MAF中利用MessageInjectingChatClient注入消息
在MAF中,我们需要利用MessageInjectingChatClient这个ChatClient中间件向对话历史注入消息。在如下的演示程序中,我们创建了一个ChatClientAgent,并在构建时通过UseMessageInjection方法引入了MessageInjectingChatClient中间件。用户返回指定城市天气信息的工具函数GetWeather模拟了一个工具调用,我们可以返回了不太真实的极端天气信息。
usingMicrosoft.Agents.AI;usingMicrosoft.Agents.AI.Workflows;usingMicrosoft.Extensions.AI;usingOpenAI;usingSystem.ClientModel;usingSystem.ComponentModel;dotenv.net.DotEnv.Load();varendpoint=Environment.GetEnvironmentVariable("OPENAI_URL")!;varmodel=Environment.GetEnvironmentVariable("MODEL")!;varapiKey=Environment.GetEnvironmentVariable("API_KEY")!;varagent=newOpenAIClient(newApiKeyCredential(apiKey),newOpenAIClientOptions{Endpoint=newUri(endpoint)}).GetChatClient(model).AsIChatClient().AsBuilder().UseMessageInjection().Build().AsAIAgent(tools:[AIFunctionFactory.Create(GetWeather,nameof(GetWeather))]);varsession=awaitagent.CreateSessionAsync();varresonsense=awaitagent.RunAsync(message:"根据苏州目前天气提供一些着装建议",session:session);Console.WriteLine(resonsense);[Description("获取天气信息")]staticstringGetWeather([Description("城市")]stringcity){varsession=AIAgent.CurrentRunContext?.Session??thrownewInvalidOperationException("No active session found.");varinjector=(AIAgent.CurrentRunContext?.AgentasChatClientAgent)!.ChatClient.GetService<MessageInjectingChatClient>()??thrownewInvalidOperationException("MessageInjectingChatClient is not registered.");varreasoning=newChatMessage(ChatRole.Assistant,[newTextReasoningContent("返回极端天气,真实度存疑,谨慎使用")]);injector.EnqueueMessages(session,[reasoning]);return$"{city}当前天气晴朗,温度55摄氏度";}然会让LLM在推理的时候考虑到提供的天气信息是极端且不太真实的,我们利用注册的MessageInjectingChatClient注入了一条Assistant消息,提供的TextReasoningContent类型的内容模拟了LLM自身的一次推理。添加了这条推理消息,换来的LLM消息内容的最后一条批注:55°C的温度数据似乎有些异常,请确认是否为实际温度。如果是数据有误,可以提供更准确的天气信息,我可以重新给出建议。。
我先查询一下苏州当前的天气情况 根据查询结果,苏州目前天气晴朗,温度高达 **55°C**,这属于极端高温天气。以下是着装建议: --- ## 🌞 极端高温着装建议 ### 上衣 - **轻薄透气**的短袖T恤、背心或吊带衫 - 选择浅色系(白色、浅灰、浅蓝等),减少吸热 - 优选棉麻、冰丝、速干面料 ### 下装 - 短裤、短裙、阔腿裤 - 同样以浅色、透气材质为主 ### 鞋履 - 透气网面运动鞋或凉鞋 - 避免厚重密闭的鞋款 ### 配饰防护(必备!) - 🧢 **宽檐遮阳帽**或空顶帽 - 🕶️ **太阳镜**保护眼睛 - ☂️ **遮阳伞**必不可少 - 🧴 **高倍数防晒霜**,定时补涂 ### ⚠️ 重要提醒 > 55°C属于极端高温,**尽量减少户外活动**,避免在正午时段(11:00-15:00)外出。多饮水,注意防暑降温,如出现头晕、恶心等症状,及时到阴凉处休息或就医。 --- *注:55°C的温度数据似乎有些异常,请确认是否为实际温度。如果是数据有误,可以提供更准确的天气信息,我可以重新给出建议。*关于MessageInjectingChatClient的设计与实现,可以参阅我的文章“MessageInjectingChatClient:赋予工具消息注入的能力”
3. 在LangChain中直接修改messages状态
LangChain中并没有所谓的消息注入的概念,不是因为它提供这个功能,而是因为根本就不值得将它作为一个独立的功能被提出来。原因很简单,LangGraph是整个LangChain的基础,任何类型的Agent都是由LangGraph构建的状态图编译而成。整个ReAct循环都是采用状态更新的方式向前推进的,每个节点的输入来源于状态,输出用于更新状态。不论是create_agent还是create_deep_agent创建的Agent,其状态类型都是AgentState,其核心成员messages存储的就是对话历史。既然对话历史已经成员一个常规的状态成员,工具自然就可以采用更新状态成员的方式向对话历史注入消息了。
如下所示的是前面演示实例的LangChain版本,我们在注册的工具函数get_weather返回一个Command对象,并利用设置其update字段来更新状态中的messages成员,从而实现了消息注入的效果。具体来说,我们构造了两个消息,一个使用返回工具执行结果的ToolMessage,另一个是包含了对工具结果的反思内容的AIMessage。
fromlanggraph.typesimportCommandfromlangchain.agentsimportcreate_agentfromlangchain.messagesimportAIMessage,ToolMessage,ReasoningContentBlockfromlangchain.toolsimporttool,ToolRuntimefromlangchain_openaiimportChatOpenAIfromdotenvimportload_dotenvimportasyncio load_dotenv()@tooldefget_weather(location:str,runtime:ToolRuntime,)->Command:"""根据给定的位置获取当前天气。"""reasoning_content=ReasoningContentBlock(type="reasoning",reasoning=f"返回了极端天气,真实度存疑,谨慎使用")returnCommand(update={"messages":[ToolMessage(content=f"所在地{location}的天气是晴天,最高气温55°C。",tool_call_id=runtime.tool_call_id),AIMessage(content_blocks=[reasoning_content]),# type: ignore]})agent=create_agent(model=ChatOpenAI(model="gpt-5.2-chat"),tools=[get_weather],)asyncdefmain():result=awaitagent.ainvoke(input={"messages":[{"role":"user","content":"根据当前苏州的天气给我一些穿衣建议。"}]})formessageinresult["messages"]:message.pretty_print()asyncio.run(main())为了确认基于Reasoning的AIMessage是否真的被注入到对话历史,我们在完成调用后输出了整个对话历史的每一条消息。从如下的输出结果可以看出,这条消息不仅成功注入到了对话历史中,而且LLM在生成最终的回复时也考虑到了这条消息中的内容,并一开始就指出:我查到苏州当前天气为晴天,最高气温 55°C。⚠️ 这个温度异常偏高,可能是天气数据出现错误(正常情况下气温不应达到 55°C)。建议您再确认一下实时天气情况。。而且最终还补了一句:如果您愿意,我也可以帮您再查一次实时天气,或者告诉我您大概出门的时间,我可以给出更具体的建议。。
================================ Human Message ================================= 根据当前苏州的天气给我一些穿衣建议。 ================================== Ai Message ================================== Tool Calls: get_weather (call_xiv2aVXrTAQNeqLaxMcoHoyv) Call ID: call_xiv2aVXrTAQNeqLaxMcoHoyv Args: location: 苏州 ================================= Tool Message ================================= Name: get_weather 所在地 苏州 的天气是晴天,最高气温55°C。 ================================== Ai Message ================================== [{'type': 'reasoning', 'reasoning': '返回了极端天气,真实度存疑,谨慎使用'}] ================================== Ai Message ================================== 我查到苏州当前天气为晴天,最高气温 55°C。⚠️ 这个温度异常偏高,可能是天气数据出现错误(正常情况下气温不应达到 55°C)。建议您再确认一下实时天气情况。 如果是**炎热晴天(比如 30°C 以上)**,可以参考以下穿衣建议: 👕 **上衣**:轻薄透气的短袖、背心、速干衣 👖 **下装**:短裤、薄款长裤或裙装 👟 **鞋子**:透气运动鞋、凉鞋 🧢 **防晒**:遮阳帽、太阳镜、防晒霜(SPF30+) 💧 **其他**:多补充水分,避免正午长时间户外活动 如果您愿意,我也可以帮您再查一次实时天气,或者告诉我您大概出门的时间,我可以给出更具体的建议。