目录1. Todo 模块的职责2. 如何理解 Todos它解决什么问题、为什么值得用3. Todos 的生命周期3.1 默认守则如何指挥模型动手3.2 状态与条目的生命周期3.3 每轮自动注入清单4. 类型清单5. 相关类型解析5.1 TodoProvider5.2 TodoProviderOptions5.3 TodoItem5.4 内部类型6. 暴露给模型的工具7. 行为要点8. 与其它模块的关系9. 扩展与最佳实践10. 小结上一篇基于 MAF .NET1.13.0实验性 APIMAAI001程序集Microsoft.Agents.AI核心包 · 源码目录dotnet/src/Microsoft.Agents.AI/Harness/Todo/1. Todo 模块的职责给智能体一份会话级待办清单让它在执行多步复杂任务时能把任务拆成可追踪的条目、逐项完成、随时增删。它本质上是一个AIContextProvider每次调用前往系统指令里注入怎么用待办清单的守则并把 5 个待办操作工具暴露给模型同时在每轮调用开头注入一条合成消息把当前待办列表念给模型听确保它始终知道还有哪些事没干完。待办状态存在会话状态袋AgentSessionStateBag里跨同一会话的多次调用保持不同会话互相隔离。2. 如何理解 Todos把 Todos 想象成智能体随手贴在桌面上的一叠便利贴清单——接到一个多步骤的活儿时它先把活儿拆成一条条“待办”写下来干完一条划掉一条你随时能凑过去看它写了什么、还剩几条没干。举个例子。你对一个研究助理说“帮我调研 A、B、C 三家云厂商的 Serverless 冷启动延迟最后给一份对比报告。”这是个典型的多步骤任务。智能体不会闷头一次写完而是在默认守则引导下先判断“这活儿复杂”于是调todos_add把它拆成一串待办1 [open] 调研 A 厂商冷启动延迟 2 [open] 调研 B 厂商冷启动延迟 3 [open] 调研 C 厂商冷启动延迟 4 [open] 汇总对比、撰写报告接下来它一条条推进查完 A 就调todos_complete把 #1 划成 done再往下走。你中途插一句“顺便加上 D 厂商”它就调todos_add添一条你要是改主意说“算了不看 C 了”它会调todos_remove把 #3 删掉。如果你跑起了 Harness 的 Console 示例输入/todos就能看到这份清单的实时状态反过来如果你只是问一句“现在几点”这种一步就能答的简单问题智能体不会建待办——默认守则明确要求它先分辨“复杂 vs 简单”简单的直接做别为难自己搞一堆清单。它解决什么问题、为什么值得用大语言模型的“记性”受上下文窗口限制长任务里很容易忘掉前面定好的步骤、漏掉某一环、或跑着跑着跑偏。Todos 的本质是把计划从“对话里的一段话”外化成一份结构化、会持久化的状态好处有四不掉步、不跑偏每轮调用开头MAF 都会把当前清单重新“念”给模型一条合成 user 消息它始终清楚“还剩哪些没干”。天然支持“先规划、后执行”把一个模糊的大请求变成可确认、可追踪的计划——配合AgentMode的 plan / execute 就是一条完整的规划闭环。抗上下文压缩待办存在会话状态袋里不是普通对话消息因此不会被 Compaction 当作旧消息压缩掉哪怕历史被砍计划还在。可自主、可观测Loop 能读它判断“是否全部干完”来决定要不要再跑一圈无人值守续跑宿主代码 / UI 也能读它给人看实时进度。一句话Todos 让智能体从“一次性尽力而为”变成“有计划、能追踪、可恢复、看得见”。3. Todos 的生命周期Todos 的“创建 → 改状态 → 删除”全部由模型的工具调用驱动而“每轮把清单念给模型”则由MAF 自动注入。下面按时间线拆开。3.1 默认守则如何指挥模型动手TodoProvider注入的DefaultInstructions决定了模型“何时该建 / 改 / 删”先判断任务是复杂多步还是简单一步。复杂 → 拆成待办、todos_add进清单简单 → 不建待办直接做。需要澄清时先问用户再据此建待办。用户对计划有反馈 → 增删条目调整用户切换话题 / 改主意 → 移除无关项、清空或重建清单。执行中随手todos_complete标记完成、todos_remove删掉不再需要的。3.2 状态与条目的生命周期阶段触发者发生了什么状态创建首次访问第一次调用 Provider / 工具时GetOrInitializeState懒创建一个空TodoStateItems[]、NextId1存进会话状态袋key TodoProvider条目创建todos_add每条分配Id NextId从 1 起Title / Description 去首尾空白IsComplete默认 false写回状态状态变更todos_complete按 ID 把未完成项的IsComplete置 true只有确实完成 ≥1 条才写回。reason只引导模型说清“怎么完成的”不落盘条目删除todos_remove按 ID 批量删除删掉 ≥1 条才写回。ID 不回收NextId只增不减删了也不复用清空todos_remove没有专门的 clear 工具——“清空”就是模型把条目全删掉底层TodoState与NextId仍在不重置状态销毁会话结束待办状态随会话生命周期存在跨同一会话多次调用一直保持MAF 不做自动过期 / 清理。会话被丢弃时状态随之消失不同会话相互隔离3.3 每轮自动注入清单每次调用前MAF 把当前清单格式化成一条合成 user 消息注入形如### Current todo list加上- {id} [open/done] {title}: {desc}空清单则为- none yet受SuppressTodoListMessage/TodoListMessageBuilder控制。这一步不增删条目但它是“模型每轮都记得清单”的关键。关键点清单不会“自动清理”。一条待办从建立到消失中间每一次状态变化都对应模型的一次显式工具调用MAF 只负责持久化和每轮提醒不替模型做增删决策。4. 类型清单类型可见性种类职责TodoProviderpublicAIContextProvider,IDisposable模块主体注入指令、暴露工具、维护状态TodoProviderOptionspublic配置类自定义指令、是否注入清单消息、清单消息格式TodoItempublic数据模型单个待办项Id / Title / Description / IsCompleteTodoStateinternal会话状态持有ListTodoItem与自增NextIdTodoItemInputinternal工具入参todos_add的入参Title / DescriptionTodoCompleteInputinternal工具入参todos_complete的入参Id / Reason5. 相关类型解析5.1 TodoProvider模块主体继承AIContextProvider、实现IDisposable。核心职责有三注入上下文覆写的ProvideAIContextAsync返回一个AIContext里面装着待办使用守则Instructions、5 个工具Tools以及默认情况下一条合成 user 消息——把当前待办列表格式化后注入让模型每轮开头就看到还剩哪些没干。维护状态通过ProviderSessionStateTodoState在会话状态袋里申请一个独立 key 存取TodoState。线程安全所有读写都用每会话一把锁SemaphoreSlim按AgentSession用ConditionalWeakTable缓存无会话时用一把兜底锁序列化避免并发产生重复 ID、丢更新或脏读。它还对外暴露两个公开方法供宿主代码绕过模型直接读状态官方示例的/todos控制台命令即用此实现GetAllTodosAsync(session, ct)—— 取全部待办含已完成。GetRemainingTodosAsync(session, ct)—— 只取未完成的。注意这两个方法返回的是内部状态里的活引用live reference改它们的属性会直接改动 Provider 状态。5.2 TodoProviderOptions控制TodoProvider行为的可选配置Instructionsstring?—— 整段替换默认注入的待办守则。默认守则会教模型先判断任务复杂度复杂才拆 todo简单直接做。SuppressTodoListMessagebool—— 关掉每轮注入当前清单的合成消息。默认false即会注入。TodoListMessageBuilderFuncIReadOnlyListTodoItem, string?—— 自定义那条清单消息的文本格式。不设则用内置格式化。5.3 TodoItem公开数据模型一个待办项就是它Idint—— 会话内自增的唯一标识从 1 起。Titlestring—— 标题。Descriptionstring?—— 可选描述。IsCompletebool—— 是否已完成。字段都带[JsonPropertyName]因为它要随会话状态序列化持久化。5.4 内部类型TodoState—— 会话状态本体持有ItemsListTodoItem和NextId下一个要分配的自增 ID从 1 起。TodoItemInput——todos_add工具的入参形状Title 可选Description。TodoCompleteInput——todos_complete工具的入参形状IdReason。6. 暴露给模型的工具TodoProvider通过AIFunctionFactory.Create(...)动态生成 5 个工具工具名入参返回说明todos_addListTodoItemInput新建的TodoItem列表一次可加一个或多个自动分配自增 IDtodos_completeListTodoCompleteInput实际标记完成的条数按 ID 把未完成项标记为完成todos_removeListintID 列表实际删除的条数按 ID 删除待办项todos_get_remaining无未完成的TodoItem列表查还剩哪些todos_get_all无全部TodoItem列表查全量含已完成7. 行为要点ID 从 1 起自增由TodoState.NextId维护删除不回收 ID。todos_complete的reason不持久化入参里要Reason但代码只把对应项的IsComplete置为true并不存这个理由。它的作用是引导模型说清为什么算完成了对模型推理与日志有益而非写进状态。这是个容易误以为理由被存下来了的反直觉点。每轮注入合成消息默认每次调用都会把当前清单作为一条 user 消息注入受SuppressTodoListMessage/TodoListMessageBuilder控制。这依赖管线里的消息注入能力把第三方造的 user 消息插到正确位置。批量友好增 / 删 / 完成都支持一次传多个鼓励模型在一次工具调用里处理一批减少往返。8. 与其它模块的关系HarnessAgent门面默认装配TodoProvider用HarnessAgentOptions.DisableTodoProvider关闭。LoopTodoCompletionLoopEvaluator读TodoProvider的状态判断待办是否全部清空来决定循环是否继续。AgentMode默认的plan模式守则里就要求把任务拆成 todo两者在先规划后执行工作流里配合。Console 脚手架/todos命令通过agent.GetServiceTodoProvider()拿到 Provider 后调GetAllTodosAsync不发起模型调用就打印清单。9. 扩展与最佳实践可定制的三个扩展点都在TodoProviderOptions构造TodoProvider时传入Instructions—— 整段替换默认守则。想改语气、改成中文、或收紧 / 放宽“何时拆 todo”的纪律时用它不传就用内置守则已覆盖大多数场景。SuppressTodoListMessage—— 关掉“每轮注入当前清单”的合成消息。默认不要关它是模型不掉步的关键。只有当你已用别的方式让模型看到进度、又想省 token 时才考虑关。TodoListMessageBuilder—— 自定义那条清单消息的文本格式比如换成表格、加优先级列。最佳实践把 Todos 当“工作便签”别当持久业务数据。它是会话级、模型驱动的会话结束即失、reason不落盘、ID 删了不回收。需要审计或长期留存的清单另建业务存储别指望 Todo 状态。做实时待办 UI 就走读方法。用GetAllTodosAsync/GetRemainingTodosAsync直接读别为了拿清单去发一次模型调用。注意返回的是内部状态的活引用——UI 只读、别改属性否则会串改 Provider 状态。配合 Loop 自主续跑时务必设安全阀。TodoCompletionLoopEvaluator会“待办没清完就再跑一圈”一定要给LoopAgentOptions.MaxIterations兜底避免模型迟迟不收敛导致空转。待办的增 / 改 / 删交给模型经工具完成。模块对宿主只开放了读方法无宿主侧的增 / 删 API这样清单与模型的认知才不会脱节。10. 小结Todo 是 Harness 里最轻、最独立的一块积木一个AIContextProvider 5 个工具 一份会话状态解决长任务里模型容易忘记自己要干什么的问题。它的设计取舍很清楚——状态会话隔离、操作批量化、每轮把清单念给模型、并发用每会话锁兜底同时把GetAllTodosAsync等读方法开放给宿主方便做实时待办 UI。下一篇引入地址