当LLM需要执行一个包含多个步骤的复杂任务时,随着对话轮次的增加,提示词会变得极长,LLM极易发生注意力涣散,导致漏掉某些步骤或重复执行已完成的步骤。解决的方法也很简单,就是创建一个TodoList,把每一步都列出来,LLM每完成一步,就在对应的代办事项上做个标记,这样就能时刻提醒LLM当前的进度和剩余的步骤了。作为LangChain的Harness框架的Deep Agents提供了一个TodoListModdleware来实现这个功能,MAF中与之对应的则是TodoProvider。
1. 采用TodoList的方式完成自动化部署
在如下这个演示实例中,我们创建了一个TodoProvider来帮助Agent实现基于DevOps的自动换服务部署流程。我们定义了四个工具函数PullLatestCode、BuildDockerImage、DeployToK8s和RunHealthCheck,分别用来完成部署流程中的四个步骤:拉取代码、构建镜像、部署发布和健康检查。我们在创建Agent的时候注册了TodoProvider,还在系统指令中明确了LLM需要利用TodoProvider提供的工具将部署流程拆解成详细的Todo任务列表,并要求每完成一步骤就必须立刻调用内置的打勾工具将该任务标记为完成。
usingdotenv.net;usingMicrosoft.Agents.AI;usingMicrosoft.Extensions.AI;usingOpenAI;usingSystem.ClientModel;usingSystem.ComponentModel;DotEnv.Load();varmodel=Environment.GetEnvironmentVariable("MODEL")!;varapiKey=Environment.GetEnvironmentVariable("API_KEY")!;varendpoint=Environment.GetEnvironmentVariable("OPENAI_URL")!;vardeployAgent=newOpenAIClient(newApiKeyCredential(apiKey),newOpenAIClientOptions{Endpoint=newUri(endpoint)}).GetChatClient(model).AsIChatClient().AsAIAgent(options:newChatClientAgentOptions{AIContextProviders=[newTodoProvider()],ChatOptions=newChatOptions{Tools=[AIFunctionFactory.Create(PullLatestCode,nameof(PullLatestCode)),AIFunctionFactory.Create(BuildDockerImage,nameof(BuildDockerImage)),AIFunctionFactory.Create(DeployToK8s,nameof(DeployToK8s)),AIFunctionFactory.Create(RunHealthCheck,nameof(RunHealthCheck))],Instructions=""" 你是一个高可靠的DevOps部署专家。1.当接收到用户的部署请求后,你必须首先使用系统内置的Todo工具,将整个部署流程拆解为详细的TODO任务列表(例如:拉取代码、构建镜像、部署发布、健康检查)。2.拆解完成后,你必须严格按照TODO列表的顺序,每次调用一个业务工具去执行。3.每当一个业务工具执行成功并返回结果后,你**必须**立刻调用内置的打勾工具将该任务标记为完成。4.严禁跳过步骤,也严禁在旧步骤未标记完成时盲目推进下一步。"""}});varresponse=awaitdeployAgent.RunAsync("部署Order-Server到Staging环境");Console.WriteLine(response);[Description("从 Git 仓库拉取最新代码")]staticstringPullLatestCode(stringserviceName){Console.WriteLine($"成功拉取`{serviceName}`的源代码");return"SUCCESS: 成功拉取main分支最新代码,Commit ID: a7f89c2";}[Description("构建 Docker 镜像并推送到私有仓库")]staticstringBuildDockerImage(stringserviceName){Console.WriteLine($"成功构建了`{serviceName}`的Docker镜像");return"SUCCESS: 镜像构建成功,已推送到私有仓库";}[Description("将服务部署到指定的Kubernetes集群")]staticstringDeployToK8s(stringserviceName,stringcluster){Console.WriteLine($"成功将`{serviceName}`部署到`{cluster}`集群 ");return$"SUCCESS:{serviceName}的 ReplicaSet 已在集群{cluster}中成功拉起。";}[Description("运行部署后健康检查,确保服务正常运行")]staticstringRunHealthCheck(stringserviceName){Console.WriteLine($"成功完成`{serviceName}`的健康检查");return"SUCCESS: /healthz 接口返回 200 OK,服务运行平稳。";}最终我们通过调用Agent下达了一个部署的指令,最终会生成如下的输出结果。其中前面四行为每个步骤的工具函数输出的结果,后面则是Agent的最终响应结果。可以看到,Agent能够非常清晰地按照TodoList的步骤来执行,并且在每一步完成后都能正确地标记完成状态,这极大地提升了Agent执行复杂任务的可靠性。
成功拉取`Order-Server`的源代码 成功构建了`Order-Server`的Docker镜像 成功将`Order-Server`部署到`Staging`集群 成功完成`Order-Server`的健康检查 ✅ **Order-Server 已成功部署到 Staging 环境** 部署流程执行结果如下: 1. ✅ 已拉取最新代码(Commit ID: `a7f89c2`) 2. ✅ Docker 镜像构建完成并成功推送至私有仓库 3. ✅ 成功部署至 Kubernetes Staging 集群(ReplicaSet 正常运行) 4. ✅ 健康检查通过(`/healthz` 返回 200 OK) 当前服务运行状态正常,可用于测试与验证。 如需执行回滚、发布到生产环境或查看部署日志,请告知。2. 查看执行流程
我们可以按照如下的方式,通过输出响应结果消息列表的方式查看Agent的执行流程,看看它是否按照TodoList的步骤来执行的。如下所示,我们可以看到Agent在执行过程中确实是按照TodoList的步骤来执行的,并且每完成一步都会调用内置的打勾工具将该任务标记为完成。
...varresponse=awaitdeployAgent.RunAsync("部署Order-Server到Staging环境");varindex=1;foreach(varmessageinresponse.Messages){varrole=message.Role;varcontent=message.Contents.Single();varformattedContent=contentswitch{TextContenttextContent=>$"{index++}. [{role}]{textContent.Text}",FunctionCallContentfunctionCallContent=>$"{index++}. [{role}]Function Call:{functionCallContent.Name}[{functionCallContent.CallId}]",FunctionResultContentfunctionResultContent=>$"{index++}. [{role}]Function Result:{functionResultContent.Result}",_=>content.ToString()??""};Console.WriteLine(formattedContent);}输出:
1. [assistant]Function Call: todos_add[call_wkA5Q3J7HXP20j0LTKjfyvFF] 2. [tool]Function Result:[{"id":1,"title":"拉取Order-Server最新代码","description":"从Git仓库拉取Order-Server服务的最新代码","isComplete":false},{"id":2,"title":"构建Order-Server Docker镜像","description":"构建Docker镜像并推送到私有仓库","isComplete":false},{"id":3,"title":"部署Order-Server到Staging集群","description":"将服务部署到Kubernetes Staging集群","isComplete":false},{"id":4,"title":"运行Order-Server健康检查","description":"部署完成后执行健康检查确保服务正常","isComplete":false}] 3. [assistant]Function Call: PullLatestCode[call_A4rYbY9a9hj8Hd5Mhbj2c3vd] 4. [tool]Function Result:SUCCESS: 成功拉取main分支最新代码,Commit ID: a7f89c2 5. [assistant]Function Call: todos_complete[call_WepTRxmzdrZYJ4qrMZo7lwVf] 6. [tool]Function Result:1 7. [assistant]Function Call: BuildDockerImage[call_G6jyBy0FE9eXMKl151bhWDDc] 8. [tool]Function Result:SUCCESS: 镜像构建成功,已推送到私有仓库 9. [assistant]Function Call: todos_complete[call_1JwMYR6Hu3wLpmWMnyOjFtuL] 10. [tool]Function Result:1 11. [assistant]Function Call: DeployToK8s[call_cRvrHaazKzama0VKwMFE8ulx] 12. [tool]Function Result:SUCCESS: Order-Server 的 ReplicaSet 已在集群 Staging 中成功拉起。 13. [assistant]Function Call: todos_complete[call_T0fl6nHFnBwNf07GscMpI115] 14. [tool]Function Result:1 15. [assistant]Function Call: RunHealthCheck[call_FWrwU6EdQUaS7Tohlo1A5Q3F] 16. [tool]Function Result:SUCCESS: /healthz 接口返回 200 OK,服务运行平稳。 17. [assistant]Function Call: todos_complete[call_thWYKXutloE7J4ADRLIpvhVv] 18. [tool]Function Result:1 19. [assistant]✅ **Order-Server 已成功部署到 Staging 环境** 部署流程执行结果如下: 1. ✅ 成功拉取最新代码(Commit ID: `a7f89c2`) 2. ✅ Docker 镜像构建完成并推送至私有仓库 3. ✅ 成功部署至 Kubernetes Staging 集群(ReplicaSet 运行正常) 4. ✅ 健康检查通过(`/healthz` 返回 200 OK) 📦 当前服务状态:**运行正常** 🚀 环境:**Staging** 🟢 健康状态:**Healthy** 如需执行回滚、扩容、副本调整或发布到 Production,请告知。通过生成的19条消息,我们可以大体归纳出Agent的执行流程:
- 调用
todos_add工具函数将待办事项添加到TodoList中,并作为Session状态存储下来; - 针对TodoList创建一个循环执行每一项代办事项:
- 调用对应的业务工具函数去执行当前的待办事项;
- 每当一个工具函数执行完成后,立刻调用
todos_complete工具函数将该事项标记为完成状态;
- 所有待办事项完成后,生成最终的响应结果。
todos_add和todos_complete这两个工具函数是TodoProvider注册的五个工具函数中的两个,其他三个分别是todos_remove、todos_get_remaining和todos_get_all,分别用于从TodoList中移除事项、获取未完成的事项列表和获取所有事项列表。
3. TodoList状态的维持
当TodoProvider根据任务创建的TodoList是一个TodoItem对象的集合,每个TodoItem表示一个代办事项。TodoItem类型定义如下,我们可以利用它得到每个代办事项的ID、标题、描述和完成状态等信息。
publicsealedclassTodoItem{publicintId{get;set;}publicstringTitle{get;set;}=string.Empty;publicstring?Description{get;set;}publicboolIsComplete{get;set;}}当TodoList被创建出来后,Agent会按部就班地执行每个事项,并在每个事项完成后调用todos_complete工具函数将该事项标记为完成状态。除此之外,TodoProvider还提供了删除代办事项的工具函数todos_remove,所以TodoProvider需要将利用Session来维持这个TodoList的实时状态。具体的状态类型为如下这个TodoState,除了一个TodoItem对象的集合来表示当前的TodoList之外,还有一个NextId自增字段用来生成新的TodoItem的ID。
internalsealedclassTodoState{publicList<TodoItem>Items{get;set;}=[];publicintNextId{get;set;}=1;}StateKeys属性返回存储TodoState的状态键,默认为TodoProvider类型名称。TodoProvider利用_sessionState字段返回的ProviderSessionState<TodoState>对象来维持TodoList和NextId的状态。每当由代办事项的添加、删除和改变,都会利用它来对整个状态实施全量更新。GetAllTodosAsync和GetRemainingTodosAsync方法则分别用来获取当前的TodoList和未完成的事项列表,它们正是利用了此对象来提取原始的TodoList。
publicsealedclassTodoProvider:AIContextProvider,IDisposable{privatereadonlyProviderSessionState<TodoState>_sessionState;privateIReadOnlyList<string>?_stateKeys;publicoverrideIReadOnlyList<string>StateKeys=>this._stateKeys??=[this._sessionState.StateKey];publicasyncTask<IReadOnlyList<TodoItem>>GetAllTodosAsync(AgentSession?session);publicasyncTask<List<TodoItem>>GetRemainingTodosAsync(AgentSession?session);}4. TodoProviderOptions
在介绍TodoProvider的使用方式之前,我们先来看一下它的配置选项TodoProviderOptions。作为TodoProvider提供的输入增强的手段,它不仅仅用来注册管理TodoList的工具函数和设置对应的系统指令,如果将SuppressTodoListMessage设置为true,它还可以根据当前TodoList的状态创建一个角色为User的消息,这样LLM可以更加直观地了解到当前的TodoList状态,从而更好地指导它的行动。
publicsealedclassTodoProviderOptions{publicstring?Instructions{get;set;}publicboolSuppressTodoListMessage{get;set;}publicFunc<IReadOnlyList<TodoItem>,string>?TodoListMessageBuilder{get;set;}}如果没有利用Instructions属性对系统指令作显式设置,TodoProvider会默认使用如下所示的指令文本:
## Todo Items You have access to a todo list for tracking work items. While planning, make sure that you break down complex tasks into manageable todo items and add them to the list. Ask questions from the user where clarification is needed to create effective todos. If the user provides feedback on your plan, adjust your todos accordingly by adding new items or removing irrelevant/old ones. During execution, use the todo list to keep track of what needs to be done, mark items as complete when finished, and remove any items that are no longer needed. When a user changes the topic or changes their mind, ensure that you update the todo list accordingly by removing irrelevant/old items or adding new ones as needed. Use these tools to manage your tasks: - Use todos_add to break down complex work into trackable items (supports adding one or many at once). - Use todos_complete to mark items as done when finished (supports one or many at once). Include a reason describing how the items were completed. - Use todos_get_remaining to check what work is still pending. - Use todos_get_all to review the full list including completed items. - Use todos_remove to remove items that are no longer needed (supports one or many at once).SuppressTodoListMessage属性默认为false,如果没有将其设置为true,我们可以利用TodoListMessageBuilder属性指定的委托来生成消息文本。如果没有显式设置TodoListMessageBuilder属性,TodoProvider会默认使用如下所示的方式来生成消息文本:
internalstaticstringFormatTodoListMessage(List<TodoItem>items){if(items.Count==0){return"### Current todo list\n- none yet";}varsb=newStringBuilder("### Current todo list\n");foreach(variteminitems){stringstatus=item.IsComplete?"done":"open";sb.Append($"-{item.Id}[{status}]{item.Title}");if(!string.IsNullOrWhiteSpace(item.Description)){sb.Append($":{item.Description}");}sb.AppendLine();}returnsb.ToString().TrimEnd();}5. TodoProvider
TodoProvider针对LLM输入的增强(工具、系统指令和针对当前TodoList生成的消息)体现在重写的ProvideAIContextAsync方法返回的AIContext对象中。
publicsealedclassTodoProvider:AIContextProvider,IDisposable{protectedoverrideasyncValueTask<AIContext>ProvideAIContextAsync(InvokingContextcontext,CancellationTokencancellationToken=default);}具体的逻辑很简单:
- 如果利用
TodoProviderOptions的Instructions属性对系统指令进行了显式设置,则直接使它,否则使用默认的指令文本; - 注册
todos_add、todos_complete、todos_get_remaining、todos_get_all和todos_remove这五个工具函数; - 如果
TodoProviderOptions的SuppressTodoListMessage属性没有被设置为true:- 如果提供了
TodoListMessageBuilder,则利用它来生成消息文本 - 否则使用默认的方式来生成消息文本;
- 如果提供了
下面列出了五个工具函数的描述文本和输入参数的JSON Schema:
------------------------------ Tool: todos_add ------------------------------ Description: Add one or more todo items. Each item has a title and an optional description. Returns the list of created todo items. JsonSchema: { "type": "object", "properties": { "todos": { "type": "array", "items": { "type": "object", "properties": { "title": { "type": "string" }, "description": { "type": [ "string", "null" ] } } } } }, "required": [ "todos" ] } ------------------------------ Tool: todos_complete ------------------------------ Description: Mark one or more todo items as complete. Each entry has an ID and a reason describing how/why the item was completed. Returns the number of items that were found and marked complete. JsonSchema: { "type": "object", "properties": { "items": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "reason": { "type": "string" } } } } }, "required": [ "items" ] } ------------------------------ Tool: todos_remove ------------------------------ Description: Remove one or more todo items by their IDs. Returns the number of items that were found and removed. JsonSchema: { "type": "object", "properties": { "ids": { "type": "array", "items": { "type": "integer" } } }, "required": [ "ids" ] } ------------------------------ Tool: todos_get_remaining ------------------------------ Description: Retrieve the list of incomplete todo items. JsonSchema: { "type": "object", "properties": {} } ------------------------------ Tool: todos_get_all ------------------------------ Description: Retrieve the full list of todo items, both complete and incomplete. JsonSchema: { "type": "object", "properties": {} }