本文由 AI 根据开发对话和测试记录整理编写。在尝试开发一个排班 Agent 应用时,我遇到了一些问题,这里记录当时如何处理。这些做法只针对具体场景,并不代表最好的解决方法。
一、UI 和 Agent 不在同一个世界里
最早暴露的问题,是 Agent 无法感知用户已经在 UI 中完成的操作。
排班修改采用的是“先生成变更预览,再由用户点击应用”的交互。一次正常流程应该是:
- Agent 调用排班工具,生成待确认的排班草稿;
- UI 展示变更明细和“应用”按钮;
- 用户点击应用,前端把草稿写入 IndexedDB;
- Agent 在下一轮对话中基于新排班继续工作。
实际使用时,用户点击应用后回复一句“已经应用了”,Agent 却仍然回答:“请点击应用按钮完成修改。”
一开始我把它理解为模型没有听懂用户的话,于是尝试加强 Prompt,告诉它用户说“已应用”时不要重复提醒。但这个办法并不可靠。因为模型不是没听懂,而是它根本看不到按钮点击产生的状态变化。每一轮对话开始时,它只拿到一份排班快照和聊天记录,不知道 UI 中的 pending diff 是否已经被提交,也不知道某个按钮刚刚被点击。
这里真正的问题不是语言理解,而是系统状态没有进入 Agent 的世界模型。
后来的处理方式是把“当前是否存在未应用变更”作为明确状态注入上下文:有草稿时可以提示用户应用;没有草稿时禁止再次提示。同时,“应用”按钮不再根据回复里是否出现某些关键词来决定是否展示,而是完全由真实 diff 驱动。
这次调整让我形成了第一个重要认识:
Agent 无法直接观察的外部事件,必须通过结构化状态回灌给它。聊天记录不能代替应用状态。
类似问题并不限于“应用”按钮。页面切月、用户手动修改数据、撤销操作、文件上传状态,都不能假设 Agent 会从自然语言中准确推断。只要 UI 和 Agent 分别维护状态,就必须设计清晰的同步通道。
二、最危险的不是工具调用失败,而是工具根本没调用
另一个非常具有迷惑性的问题,是模型会“空口完成任务”。
用户说“帮我把张医生周五改成夜班”,模型可能回复:
已为张医生调整完成,请在变更预览中确认并应用。
这句话听起来完全正常,但检查系统状态后会发现:没有工具调用,没有 pending schedule,也没有 diff。模型只是生成了一句符合语境的完成文案。
最初我仍然想通过 Prompt 解决,例如反复强调“涉及数据操作必须调用工具”“不得声称未执行的操作已经完成”。这能降低概率,却不能构成保证。LLM 的输出本质上仍然是生成结果,它可能遵守指令,也可能在复杂上下文、模型差异或流式调用中偏离指令。
最终我把“是否执行”从语言问题改造成了协议问题:
- 如果用户请求涉及查询、修改、导入、导出或统计,Agent 必须显式调用真实工具;
- 如果确实不需要工具,或者信息不足需要澄清,也必须通过一个专门的
respond_without_tool出口明确决策; - 如果模型第一次只返回文本而没有做工具决策,运行循环会要求它重新作出显式选择;
- 最终回复还要经过守卫校验:没有真实 diff 时,不允许声称排班已经生成,也不允许提示点击一个并不存在的应用按钮。
这里的关键变化,是不再把模型文字当成事实来源。
“已经执行”必须由工具结果证明,“已经修改”必须由数据差异证明,“可以应用”必须由真实草稿证明。
工具调用失败至少是可见的;工具假调用更危险,因为它会给用户制造系统已经完成工作的错觉。对于会修改数据的 Agent,真实性验证应当是运行时机制,而不只是 Prompt 中的一句要求。
三、不是所有“理解”都应该交给 LLM
Excel 导入是整个开发过程中踩坑最多的部分,也最能体现我对 LLM 职责边界的认识变化。
最初的思路很自然:把 Excel 转成文本交给模型,让模型识别表头、医生、日期和班次,再调用排班工具逐项写入。模型确实能读懂大部分表格,但很快出现了几个问题:
- 日期表头可能只有
1、2、3,模型需要猜测它们对应的年月; - 一个 30 人、31 天的表有近千个单元格,逐格生成工具参数很容易超过 Token 或调用轮次上限;
- “行政”“常班”“日班”可能是同一班次,也可能是科室自定义班次;
- 表格尾部可能有“二线”“备班”这样的角色点名行,日期格里填的是人名而不是班次;
- 多个工作表或重复表头可能干扰数据区域识别;
- 一个字的人名简称可能同时匹配多位医生。
开始时,我不断补 Prompt:告诉模型如何判断日期、如何识别表头、如何处理别名、什么时候询问用户。Prompt 越来越长,但可靠性没有成比例提高。因为这些任务中混杂了两类完全不同的问题:
第一类是确定性问题,例如日期格式转换、表头定位、数据区域扫描和已知别名查找;第二类才是不确定性问题,例如一个模糊简称究竟指谁、某个科室特有写法应该对应哪个班次。
后来,Excel 导入逐渐改成“模型决策、代码解析”的结构:
- 模型只看到少量预览,避免整份表进入上下文;
- 完整数据作为附件保留在执行器上下文中;
- 数字日期在进入模型之前就被标准化为完整日期;
- 导入工具在本地重新识别表头、医生列、日期列和工作表;
- 已知别名和无歧义映射由确定性代码处理;
- 只有多候选、未知实体和业务语义不明确时才询问用户;
- “二线/备班”一类长期规则可以持久化,下次导入直接复用。
这个过程改变了我对 Agent 智能的理解。Agent 的价值不是替代所有代码,而是在确定性系统无法自行决定的地方提供语义判断。
能通过程序稳定计算的事情,就不应该让 LLM 每次重新猜一遍。
LLM 更适合处理意图、语义和歧义;日期转换、约束检查、数据扫描和持久化一致性,仍然应该交给普通代码。
四、工具不是 API 的简单翻译,颗粒度应该贴近业务动作
工具设计早期很容易从已有代码出发:系统里有“分配单个班次”的函数,就暴露一个 assign_shift;有“新增医生”的函数,就暴露一个 add_doctor。这种设计简单,却不一定适合 Agent。
工具过细时,模型为了完成一项业务任务需要规划并调用大量步骤。导入一张排班表如果逐格调用 assign_shift,不仅效率低,还可能在固定迭代上限内只完成一部分。模型还会把大量上下文花在重复构造参数上,真正用于判断冲突的空间反而变少。
于是我增加了 batch_assign、import_excel_schedule、swap_shifts、clear_doctor_range 等更接近用户意图的工具。一句“把两个人的班对调”,应该对应一个能够完整校验双方资格、请假和现有班次的原子业务动作,而不是让模型拼接两次取消和两次分配。
但工具也不是越大越好。过粗的工具会拥有太多参数和副作用,错误时难以解释,部分成功时也很难恢复。例如一个导入工具同时创建医生、创建班次、保存别名并生成排班草稿,实际上跨越了多个事务边界。
我最终更倾向于用以下问题判断工具颗粒度:
- 它是否对应用户能够理解的完整业务动作?
- 它能否在一次调用内完成必要校验?
- 失败时能否清楚说明哪些部分没有完成?
- 它的副作用是否可以被准确描述和撤销?
- 相同业务语义是否只有一条推荐路径?
Agent 工具不是把底层 CRUD 换一个 JSON Schema,而是为模型设计一层领域语言。
五、写入语义不统一,会同时困住用户和 Agent
随着工具增加,系统里逐渐出现了三种写入方式:
- 排班编辑只修改草稿,用户确认后才写入;
- 医生、班次和规则管理立即写入数据库;
- 复制到下月等特殊操作在确认覆盖后直接写入目标月份。
其中最复杂的是 Excel 导入:缺少的医生和班次会立即创建,但排班本身只进入待确认草稿。这样做有现实原因——后续排班解析需要这些实体已经存在——但它也带来了不直观的结果:用户取消排班草稿后,新建医生可能仍然留在系统中。
一开始,我更多关注“功能能不能完成”,后来才意识到,写入时机本身就是工具契约的一部分。如果契约不统一,Agent 很难准确回答“是否已经生效”,用户也无法预期“取消”究竟会撤销什么。
因此每个工具都应该明确属于哪一类:
| 类型 | 语义 |
|---|---|
| 只读 | 不改变任何业务数据 |
| 草稿写入 | 产生 diff,用户确认后生效 |
| 立即写入 | 工具成功即持久化 |
| 危险写入 | 必须携带明确的二次确认参数 |
除此之外,还应该说明它是否支持撤销、是否允许部分成功,以及重试是否幂等。
这部分目前仍不能算彻底解决。尤其是跨实体导入,理想方案可能需要真正的事务草稿:医生、班次、别名和排班全部先进入一个统一 draft,最终一次性提交。至少在做不到这一点时,系统也应把部分落库结果明确告诉用户。
六、业务约束不能依赖 Prompt
排班系统里有很多硬规则:医生请假日期不能排班,行政班医生不能被安排轮班,轮班医生只能安排允许的班次,覆盖已有排班前必须确认。
最初很容易把这些规则写进 system prompt,然后期待模型在每次操作前自行遵守。实际情况是,模型可能忘记检查,也可能因为上下文过长漏掉要求,还可能选择一条没有包含相同校验的工具路径。
于是这些规则逐步从 Prompt 下沉到执行器和领域函数中:
- 请假日期由执行器硬性拒绝;
- 医生类型和可排班次在每次写入前校验;
- 覆盖已有月份必须显式传入确认参数;
- 有未应用草稿时禁止切换编辑月份;
- 柔和配色不仅在 Prompt 中要求,还由代码提供兜底生成;
- 名称匹配遇到多候选时拒绝静默选择。
Prompt 仍然有价值,它可以帮助模型提前选择正确路径,并生成更自然的解释。但它只能是第一层引导,不能是最后一道防线。
Prompt 决定 Agent 应该怎么做,代码决定系统允许它做什么。
这和传统系统中的前端校验与服务端校验很相似:前端校验改善体验,真正的数据安全仍要由可信执行层保证。
七、并发与快照:Agent 之外的代码也在修改世界
还有一类问题不是 Agent 主动造成的,而是 Agent 执行期间,应用中的其他响应式逻辑也在工作。
这个排班系统会自动为行政班医生生成工作日排班。Excel 导入则是一个多阶段过程:先创建医生和班次,再写入排班。早期实现中,Agent 刚创建医生,React effect 就检测到数据变化并触发行政班自动排班,把一个半成品排班写入数据库;随后 Excel 导入继续执行,两条写入链发生竞争,最终可能出现覆盖或异常合并。
修复方式是在 Agent 对话执行期间暂停自动排班 effect,等对话结束后再基于完整数据重新计算。
这个问题提醒我,Agent 并不是运行在真空里。它拿到的只是某一时刻的快照,而 UI、响应式 effect、定时任务甚至另一个客户端都可能同时修改数据。仅仅保证单个工具函数正确还不够,关键写入还需要考虑版本、事务、幂等和并发控制。
八、从“修 Prompt”到“修系统”的认识变化
回顾整个过程,我的处理思路大致经历了三个阶段。
第一阶段:把问题当成模型不听话
Agent 做错时,第一反应是补 Prompt:加粗要求、增加示例、重复禁止事项。这对改善行为有帮助,但很快进入边际收益递减。Prompt 越来越长,模型仍可能在复杂任务中犯同样的错误。
第二阶段:把问题当成工具设计不够好
随后开始增加批量工具、Excel 专用工具、对调工具和清班工具,把模型需要规划的步骤压缩成领域动作。可靠性明显提高,但状态同步、写入语义和虚假完成仍然存在。
第三阶段:把 Agent 当成分布式系统中的一个不可信决策者
最后,问题才逐渐被放回软件工程框架中:
- Agent 有自己的上下文,UI 有自己的状态,需要同步;
- 模型输出不能视为执行事实,需要验证;
- 工具调用可能部分失败,需要恢复和幂等;
- 数据可能在快照之后发生变化,需要并发保护;
- 业务规则需要由可信执行层强制;
- 用户确认是一种状态迁移,而不是一句自然语言。
这里的“不可信”并不是贬义,而是一种合理的架构假设:LLM 是概率性的决策组件,系统不能要求它永远正确,正如系统不会假设网络永不失败、用户输入永远合法。
九、我现在更认可的 Agent 分层
经过这些问题后,我更倾向于把 Agent 系统分成四层:
LLM / Planner
负责理解意图、选择业务动作、处理歧义
↓
Tool Contract
定义清晰的输入、输出、副作用、确认和失败语义
↓
Domain / Executor
负责确定性计算、业务约束、事务、幂等和一致性
↓
UI / State
展示真实执行状态,并把用户操作和外部事件回灌给 Agent
在这个分层里,LLM 并不是系统的真相来源。数据库状态、工具结果和领域校验才是。LLM 的作用是让用户能够用自然语言表达意图,并在不确定信息中协助决策。
十、结语
这次开发最重要的收获,是 Agent 工程的难点并不主要在“怎样写出一个更厉害的 Prompt”,而在怎样把一个概率性的语言模型放进确定性软件系统,同时不给它超出能力边界的责任。
最后可以把这些经验压缩成几条原则:
- UI 中 Agent 看不到的状态,必须显式同步;
- 不以模型文案判断是否执行,以工具结果和数据变化为准;
- 确定性任务交给代码,LLM 只处理意图和歧义;
- 工具应贴近业务动作,而不是简单暴露底层 CRUD;
- 每个工具都要明确写入时机、确认方式、失败语义和可撤销性;
- Prompt 用于引导,业务约束必须由执行层强制;
- 把快照过期、并发修改、部分失败和重复调用当成正常情况设计。
如果再用一句话概括,就是:
不要试图通过 Prompt 让 Agent 永不犯错,而要通过系统设计让它犯错时也无法破坏事实和规则。
当关注点从“模型能不能做”转向“系统如何证明它做了、如何保证它做对”,Agent 才真正从演示功能变成可以长期维护的产品能力。
