快速结论:CrewAI 中把 `@persist` 与自定义 `@listen` 路由结合使用时,自定义路由返回的助手消息会在 `kickoff()` 返回之后才追加到状态里,因此永远不会被写入持久化数据库;内置的 `converse_turn` 路由因为在方法体内追加所以不受影响。优先检查:你的自定义 `@listen` 路由是否只返回字符串、并且每个 turn 都新建了 `Flow` 实例。
适用环境:CrewAI,涉及 `Flow`、`@persist`、`SQLiteFlowPersistence`、`handle_turn`,每个请求新建 Flow 实例的 Web 服务器模式。Issue 确认的依赖:`crewai` 实验性 `conversational` 模块、`anthropic/claude-haiku-4-5`(仅用于复现脚本,非根因)。
最快修复方案:暂无确认的一步修复方案,但可优先尝试:在自定义 `@listen` 路由方法体内直接调用 `self.append_assistant_message(…)` 而不是只返回字符串,让消息在方法快照边界内被持久化。正式修复见 PR #7026(Issue 关闭时合并)。
注意事项:上述“最快修复方案”是依据 built-in `converse_turn` 的实现方式推出的替代方案,Issue 内未明确验证;真正修复由 PR #7026 完成,合并前应避免在生产环境依赖该行为。单进程长生命周期测试无法暴露此问题,必须用“每 turn 新实例 + 读库比对”的方式验证。
问题场景
在 CrewAI 中使用对话式 `Flow`(`conversational=True`)叠加 `@persist` 持久化,自定义 `@listen` 路由正常返回字符串回复。每次对话轮次都新建 `Flow` 实例(典型 Web 服务器模式),重启或跨 turn 后,数据库里只剩用户消息,助手回复全部丢失,导致代理无法回忆起自己之前说过什么。
报错原文
Conversational Flow golden use case improvements - @persist silently drops assistant replies from custom @listen routes
turn 1 in-memory : ['user', 'assistant']
turn 2 in-memory : ['user', 'user', 'assistant'] <-- turn 1's assistant reply is gone
--- what actually persisted ---
route_conversation ['user']
do_greet ['user'] <-- custom route: reply NOT persisted
route_conversation ['user', 'user']
converse_turn ['user', 'user', 'assistant'] <-- built-in route: reply IS persisted
built-in routes persist their replies; custom routes silently don't.
原因分析
可能原因是持久化边界不匹配。`handle_turn` 在 `self.kickoff()` 返回之后才执行助手消息的追加逻辑:
object.__setattr__(self, "_assistant_reply_appended", False)
result = self.kickoff(inputs={"id": sid}, **kickoff_kwargs)
if (
result is not None
and not self._assistant_reply_appended
and self._is_public_turn_result(result)
):
self.append_assistant_message(self._stringify_result(result))
而 `@persist` 是在 `kickoff()` 内部按方法逐个快照状态的。这段追加发生在最后一次快照之后,所以永远无法进入数据库。内置 `converse_turn` 路由在方法体内部直接调用 `append_assistant_message()`(L229/L242/L251),消息属于该方法快照的一部分,所以不受影响。自定义路由只返回字符串时,依赖 post-`kickoff` 的兜底追加,于是消息丢失。Issue 作者曾怀疑路由器准确性受影响,但实测否定了这一假说,确认问题纯粹是持久化边界。另外需要确认 `@persist` 的快照是值拷贝还是引用拷贝——如果引用拷贝,则可能是快照时序竞态而非“边界外写入”,需查看 persist 装饰器实现确认。
环境排查
- Flow 实例生命周期:确认是“每个 turn 新建 Flow 实例”还是复用一个长生命周期实例——后者不会暴露此问题。
- 持久化后端:Issue 使用 `SQLiteFlowPersistence`,其他持久化后端(Postgres、Redis 等)是否同样受影响未在 Issue 中验证。
- 路由类型:区分内置 `converse_turn` 与自定义 `@listen` 路由——只有自定义路由受影响。
- 返回方式:检查自定义路由是直接返回字符串,还是在方法体内调用 `append_assistant_message()`。
- CrewAI 版本:修复 PR #7026 合并前版本均受影响,建议确认当前版本是否已包含该修复。
解决步骤
- 可优先尝试:修改自定义 `@listen` 路由,在方法体内直接调用
self.append_assistant_message(self._stringify_result(result))或类似方式,替代仅返回字符串——这样追加操作落在方法快照内部,可被持久化。 - 如果方案 1 不可行,升级到包含 PR #7026 修复的 CrewAI 版本——该 PR 是 Issue 确认的正式修复。
- 在 PR 合并之前,如需生产使用,可在每个 turn 结束后手动从内存状态读取消息并写入持久化层,绕过 `@persist` 的快照机制,但这种方式未在 Issue 中验证。
- 添加回归测试:每个 turn 新建 Flow 实例,调用 `kickoff()` 后立即通过持久化层读回状态,比对 `[m.role for m in state.messages]` 与最后一行数据库记录的 `state_json` 是否一致。
验证方法
用 Issue 中的复现脚本,在每次 turn 之后分别打印内存中的 `flow.state.messages` 和数据库中最后一条 `state_json` 的 role 列表。修复后,两个 turn 结束时数据库应显示 `[‘user’, ‘assistant’, ‘user’, ‘assistant’]` 完整历史,且每次新实例回复时助手能回忆起之前说过的话。如果只在内存中看到消息但数据库没有,说明问题仍然存在。
参考来源
crewAIInc/crewAI #6766
crewAIInc/crewAI #7026(修复 PR)
AI 工具推荐
想把多个 AI 模型放在一个入口?
GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。
推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。
这个方案解决了吗?
可以继续搜索完整报错,或查看同一工具的其他排查指南。
![[BUG] `crewai create` offers 7 retired Anthropic model ids that 404 on first call](https://www.chat-gpts.plus/wp-content/uploads/2026/08/7124-c5bd0d53-768x403.jpg)
![[Bug]: Using DeepSeek with LlamaIndex causes a 400 Bad Request because LlamaIndex calls the deprecated /v1/completions text endpoint instead](https://www.chat-gpts.plus/wp-content/uploads/2026/08/22846-c859ce1c-768x403.jpg)
