news 2026/8/26 1:30:13

对话式修改:Agent的人机协作模式 — Chat as Interface,对话不是聊天,是最高效的人机协作方式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
对话式修改:Agent的人机协作模式 — Chat as Interface,对话不是聊天,是最高效的人机协作方式

先说结论

生成一篇文章只需要一次 LLM 调用,但改好一篇文章往往需要多轮对话

很多人做内容生成 Agent,生成完就结束了 — 用户不满意只能"重新生成",结果每次都是一篇全新的文章,之前的风格、结构、措辞全丢了。这不是协作,是抽奖。

在 self-media-agent 项目里,chat/模块实现了完整的对话式修改链路:

用户提建议 → LLM 按建议编辑 → 保存修改记录 → 更新内容 → 下次生成自动进化

三个模型,一条链路,让 Agent 从"一次性生成器"变成"可协作的编辑助手"。

模型做什么存什么
ChatSession绑定到某篇内容的对话会话消息列表 + 修改记录列表
ChatMessage一条对话消息角色 + 内容 + 时间
RevisionRecord一次修改的完整记录原文 + 修改后文 + 建议

Chat as Interface — 对话不是聊天,是最高效的人机协作方式。

一、为什么"对话修改"比"重新生成"好?

重新生成的问题

用户:这篇文章太正式了 Agent:(重新生成)→ 全新文章,但风格可能又变了,之前喜欢的部分也没了 用户:不是让你全改,是让你改语气,结构别动 Agent:(重新生成)→ 又是全新文章... 用户:算了,我自己改吧

重新生成的三个致命问题

问题说明后果
风格漂移每次生成都是"从零开始"用户刚满意的风格,下次又变了
上下文丢失不记得上一版改了什么用户重复提同样的要求
全有或全无要么全接受要么全重来无法局部修改

对话修改的优势

用户:这篇文章太正式了,活泼点 Agent:(按建议编辑,保留原文结构和措辞,只调整语气) 用户:很好!但第三段太长了,缩短一点 Agent:(在上一版基础上修改,只动第三段) 用户:完美

对话修改的核心原则保留原文风格,只改建议的部分。

重新生成: 原文 → 丢弃 → 全新文章(风格不可控) 对话修改: 原文 → 保留 → 局部修改(风格延续,精确控制)

对话修改的 Prompt 设计

api/routes/chat.py_regenerate_with_suggestion— 关键在 System Prompt 的一句话:

system_prompt = ( "你是一位资深自媒体内容编辑。用户会给你一篇已有的文章和修改建议," "你需要根据修改建议对文章进行调整,保持文章的整体风格和结构," "只修改用户建议的部分。\n\n" "输出要求:\n" "1. 第一行输出修改后的标题(以「标题:」开头)\n" "2. 空一行后输出修改后的完整正文\n" "3. 正文不要使用 markdown 格式标记\n" "4. 保持原文的风格、语气和 emoji 使用习惯" )

注意第3句:"只修改用户建议的部分"。这句话是整个对话修改的灵魂 — 它告诉 LLM:你不是在写新文章,你是在编辑已有文章。

对比生成正文时的 Prompt(content/body.py):

# 生成正文:从零创作 "请创作正文内容。" ​ # 对话修改:在原文基础上编辑 "请根据修改建议调整文章,输出修改后的标题和正文。"

一个是"创作",一个是"编辑"— 同样是调 LLM,Prompt 的定位完全不同,效果也完全不同。

修改的输入输出

user_prompt = ( f"# 原始标题\n{content.title}\n\n" f"# 原始正文\n{content.body}\n\n" f"# 修改建议\n{suggestion}\n\n" )
输入: 原始标题:夏季防晒推荐 原始正文:综上所述,夏季防晒需要注意以下几点... 修改建议:太正式了,活泼点,少用书面语 ​ 输出: 标题:夏天防晒这几个坑你一定要知道 正文:姐妹们!夏天到了,防晒这事儿真不能马虎...

LLM 同时看到原文和建议— 这样它才能"在原文基础上修改",而不是"凭空写一篇新的"。

二、三个数据模型:会话、消息、修改记录

对话式修改的基础是数据模型。chat/schema.py定义了三个模型,各司其职。

模型关系图

ChatSession(绑定到某篇内容) ├── messages: list[ChatMessage] ← 对话消息列表 │ ├── ChatMessage(role=system) ← 欢迎消息 │ ├── ChatMessage(role=user) ← 用户建议 │ ├── ChatMessage(role=assistant) ← AI 回复 │ └── ... └── revisions: list[RevisionRecord] ← 修改记录列表 ├── RevisionRecord(V1→V2) ← 第一次修改 ├── RevisionRecord(V2→V3) ← 第二次修改 └── ...

一个会话绑定一篇内容— 每篇内容有自己的对话历史和修改历史,互不干扰。

ChatMessage:对话消息

class MessageRole(str, Enum): USER = "user" # 用户消息(修改建议) ASSISTANT = "assistant" # AI 回复(修改后的文章 / 确认信息) SYSTEM = "system" # 系统消息 ​ class ChatMessage(BaseModel): id: str = Field(default="", description="消息 ID") session_id: str = Field(..., description="所属会话 ID") role: MessageRole = Field(..., description="角色") content: str = Field(..., description="消息内容") created_at: datetime = Field(default_factory=datetime.now, description="创建时间")

三种角色,各管一段

角色谁说的内容
system系统"已加载文章,你可以提出修改建议"
user用户"太正式了,活泼点"
assistantAI"已根据建议修改,修改记录 #1 已保存..."

ChatMessage 只存对话文本,不存修改前后的文章全文— 文章全文存在 RevisionRecord 里,消息里只存摘要预览。这样对话列表轻量,修改记录完整。

RevisionRecord:修改记录

class RevisionRecord(BaseModel): id: str = Field(default="", description="记录 ID") session_id: str = Field(..., description="所属会话 ID") content_id: str = Field(..., description="关联内容 ID") suggestion: str = Field(..., description="用户的修改建议") original_body: str = Field(..., description="修改前正文") revised_body: str = Field(..., description="修改后正文") original_title: str = Field(default="", description="修改前标题") revised_title: str = Field(default="", description="修改后标题") created_at: datetime = Field(default_factory=datetime.now, description="修改时间")

RevisionRecord 是对话修改的核心— 它完整记录了一次修改的"前世今生":

suggestion: "太正式了,活泼点" ← 用户说了什么 original_body: "综上所述,夏季防晒..." ← 改之前长什么样 revised_body: "姐妹们!夏天到了..." ← 改之后长什么样

为什么要把原文和修改后文都存下来?— 三个原因:

  1. 版本回退:用户觉得改坏了,可以回到上一版

  2. 偏好提取:风格进化需要 diff(第9篇讲过),original_bodyrevised_body就是 diff 的来源

  3. 审计追溯:每一步修改都有据可查,知道文章是怎么一步步变成现在的样子

ChatSession:会话

class ChatSession(BaseModel): id: str = Field(default="", description="会话 ID") content_id: str = Field(..., description="关联内容 ID") persona_id: str = Field(default="", description="关联人设 ID") messages: list[ChatMessage] = Field(default_factory=list, description="消息列表") revisions: list[RevisionRecord] = Field(default_factory=list, description="修改记录") created_at: datetime = Field(default_factory=datetime.now, description="创建时间") updated_at: datetime = Field(default_factory=datetime.now, description="更新时间")

ChatSession 是一个聚合根— 它把对话消息和修改记录组织在一起,绑定到一篇内容和一个 人设。

ChatSession ├── content_id → 绑定哪篇文章 ├── persona_id → 绑定哪个人设(用于风格进化) ├── messages → 对话历史(轻量,只存文本) └── revisions → 修改历史(重量,存完整前后文)

为什么messagesrevisions分开存?— 它们的访问模式不同:

  • messages:每次打开聊天界面就要全部加载,需要轻量

  • revisions:只在查看修改历史或提取偏好时才加载,可以重量

分开存,各按需加载,互不拖累。

三、修改流程:建议→LLM编辑→版本管理→内容更新

api/routes/chat.pysend_message是整个对话修改的入口 — 一个请求完成"保存建议→编辑文章→保存记录→更新内容→提取偏好"全流程。

完整流程图

用户发送建议 │ ▼ ① 获取/创建会话 ──→ 首次修改则创建 ChatSession + 欢迎消息 │ ▼ ② 保存用户消息 ──→ ChatMessage(role=user) │ ▼ ③ LLM 编辑文章 ──→ _regenerate_with_suggestion() │ 原文 + 建议 → 修改后文 ▼ ④ 保存修改记录 ──→ RevisionRecord(原文, 修改后文, 建议) │ ▼ ⑤ 更新内容 ──→ content.body = revised_body │ save_content(content) ▼ ⑥ AI 回复消息 ──→ ChatMessage(role=assistant) │ ▼ ⑦ 自动提取偏好 ──→ StyleLearner.extract_preference_from_revision() │ 偏好写入人设(第9篇讲过) ▼ ⑧ 持久化 ──→ save_chat_session() + _maybe_persist()

8 步,一个请求,完成修改+版本管理+风格进化。用户只发了一条消息,后台做了这么多事。

代码:send_message 的核心逻辑

# api/routes/chat.py @router.post("/send", response_model=APIResponse) async def send_message(req: ChatSendRequest) -> APIResponse: state = get_state() # ① 获取原始内容 content = state.repo.store.get_content(req.content_id) # ② 获取或创建聊天会话 session = state.repo.store.get_chat_session_by_content(req.content_id) if not session: session = ChatSession( content_id=req.content_id, persona_id=content.persona_id, ) # 添加系统欢迎消息 welcome = ChatMessage( session_id=session.id, role=MessageRole.SYSTEM, content=f"已加载文章「{content.title}」,你可以提出修改建议...", ) session.messages.append(welcome) state.repo.store.save_chat_session(session) # ③ 保存用户消息 user_msg = ChatMessage( session_id=session.id, role=MessageRole.USER, content=req.message, ) session.messages.append(user_msg) # ④ 根据建议重新生成 if req.regenerate: revised_body, revised_title = await _regenerate_with_suggestion( content=content, suggestion=req.message, config=state.config, ) # ⑤ 保存修改记录(原文 + 修改后文) revision = RevisionRecord( session_id=session.id, content_id=req.content_id, suggestion=req.message, original_body=content.body, # ← 修改前 revised_body=revised_body, # ← 修改后 original_title=content.title, revised_title=revised_title, ) session.revisions.append(revision) # ⑥ 更新内容 content.body = revised_body if revised_title: content.title = revised_title content.compute_word_count() state.repo.store.save_content(content) # ⑦ AI 回复消息 ai_msg = ChatMessage( session_id=session.id, role=MessageRole.ASSISTANT, content=f"已根据你的建议修改文章,修改记录 #{len(session.revisions)} 已保存。\n\n" f"修改后正文预览:\n{revised_body[:200]}...", ) # ⑧ 自动提取偏好(第9篇讲过,这里不展开) ...

注意req.regenerate这个开关— 用户可以只提建议不修改(regenerate=False),Agent 会回复"已收到你的建议"。这个设计让对话更灵活:用户可以先提多条建议,最后一次性修改。

请求参数:ChatSendRequest

class ChatSendRequest(BaseModel): content_id: str = Field(..., description="关联内容 ID") message: str = Field(..., description="用户消息(修改建议)") regenerate: bool = Field(default=True, description="是否根据建议重新生成文章")

三个字段,简单明了

字段类型说明
content_idstr改哪篇文章
messagestr修改建议(自然语言)
regeneratebool是否立即修改(默认 True)

用户用自然语言提建议,不需要指定改哪里、怎么改— LLM 自己理解建议并定位修改位置。这是 Chat as Interface 的核心优势:用户说人话,Agent 干人事。

四、版本链:V1→V2→V3

每次修改产生一条RevisionRecord,所有修改记录构成一条版本链。

版本链的结构

V1(原始版本) │ 建议:"太正式了,活泼点" ▼ V2(第一次修改) │ 建议:"第三段太长,缩短" ▼ V3(第二次修改) │ 建议:"加一个 emoji" ▼ V4(第三次修改)

每条RevisionRecord记录了:

RevisionRecord #1: V1 → V2 suggestion: "太正式了,活泼点" original_body: V1 的正文 revised_body: V2 的正文 RevisionRecord #2: V2 → V3 suggestion: "第三段太长,缩短" original_body: V2 的正文 revised_body: V3 的正文 RevisionRecord #3: V3 → V4 suggestion: "加一个 emoji" original_body: V3 的正文 revised_body: V4 的正文

每条记录都是相邻两个版本之间的 diff— 串起来就是完整的修改历史。

版本链的三个用途

用途怎么用对应代码
版本回退取某条的original_body恢复GET /chat/revisions/{content_id}
偏好提取从 diff 提取风格偏好StyleLearner.extract_preference_from_revision()
审计追溯查看完整修改历史session.revisions

查看修改记录的 API

@router.get("/revisions/{content_id}", response_model=APIResponse) async def get_revisions(content_id: str) -> APIResponse: """获取某篇内容的修改记录""" state = get_state() session = state.repo.store.get_chat_session_by_content(content_id) if not session: return APIResponse(data=[], message="该内容暂无修改记录") return APIResponse(data=[r.model_dump(mode="json") for r in session.revisions])

一个 GET 请求,拿到完整版本链— 前端可以展示修改历史,用户可以对比任意两个版本。

版本号的演进

早期:修改#1、修改#2、修改#3 ← 只有序号,不知道改了什么 现在:V1→V2、V2→V3、V3→V4 ← 有方向感,知道是版本演进

从"修改#N"改为"V1→V2"— 不只是换个写法,是认知的转变:修改不是"打补丁",是"版本演进"。每次修改都是一个新版本,有完整的前世今生。

五、乐观更新:体验更流畅

什么是乐观更新?

悲观更新:用户发消息 → 等 AI 回复 → 显示用户消息 + AI 回复 乐观更新:用户发消息 → 立即显示用户消息 → 等 AI 回复 → 显示 AI 回复

乐观更新的核心:先显示用户的消息,不等 AI 回复 — 让用户感觉"消息发出去了",然后在后台等 AI 处理。

为什么需要乐观更新?

用户发建议 → LLM 编辑文章(2-5秒)→ 保存记录 → 提取偏好(又2-5秒)

整个修改流程可能要 5-10 秒— 如果用悲观更新,用户点发送后界面卡住 10 秒没有任何反馈,体验极差。

乐观更新让用户立即看到自己的消息,知道"发送成功了",然后耐心等 AI 回复。

后端如何配合?

后端send_message是一个同步请求 — 收到请求,处理完所有步骤,一次性返回。前端配合乐观更新:

// 前端伪代码 async function sendMessage(message) { // 1. 乐观更新:立即显示用户消息 chatMessages.push({ role: "user", content: message }); render(); // 2. 发送请求,等 AI 回复 const response = await fetch("/api/chat/send", { method: "POST", body: JSON.stringify({ content_id, message, regenerate: true }), }); const data = await response.json(); // 3. 用服务器返回的数据替换(包含 AI 回复 + 修改记录) chatMessages = data.messages; render(); }

前端先"假装"成功,后端再"真正"处理— 两者配合,体验流畅。

AI 回复中包含预览

ai_msg = ChatMessage( session_id=session.id, role=MessageRole.ASSISTANT, content=f"已根据你的建议修改文章,修改记录 #{len(session.revisions)} 已保存。\n\n" f"修改后正文预览:\n{revised_body[:200]}{'...' if len(revised_body) > 200 else ''}", )

AI 回复不只是"改好了"— 还包含修改后正文的前 200 字预览。用户不用切到文章页面就能看到修改效果,在聊天界面就能确认"改对了吗"。

revised_body[:200]— 只取前 200 字,避免消息太长刷屏。要看完整文章,切到内容页面。

六、与风格进化的联动

对话修改不是孤立的 — 每次修改都会触发风格进化(第9篇讲过),形成闭环。

联动流程

用户修改文章 │ ├──→ 保存 RevisionRecord(原文 + 修改后文) │ └──→ StyleLearner.extract_preference_from_revision(revision) │ ▼ 提取偏好(如"语气:活泼") │ ▼ 合并到人设的 style_preferences │ ▼ 下次生成自动注入偏好 → 风格越来越准

代码:修改后自动提取偏好

# api/routes/chat.py — 修改后自动提取偏好 try: from ...persona.style_learner import StyleLearner from ...llm.client import LLMClient llm_for_learn = LLMClient( base_url=state.config.llm.base_url, api_key=state.config.llm.api_key, model=state.config.llm.model, ) learner = StyleLearner(llm=llm_for_learn, store=state.repo.store) new_prefs = await learner.extract_preference_from_revision(revision) if new_prefs: persona = state.repo.store.get_persona(content.persona_id) existing = list(persona.style_preferences) existing.extend(new_prefs) merged = StyleLearner._merge_preferences(existing) updated_persona = persona.model_copy(update={ "style_preferences": merged, "updated_at": datetime.now(), }) state.repo.store.save_persona(updated_persona) except Exception as e: logger.warning(f"即时偏好提取失败(不影响主流程): {e}")

两个关键设计

  1. 偏好提取失败不影响主流程try/except兜底,提取失败只是不进化,不影响修改本身。主流程是修改,进化是附赠。

  2. 用独立的 LLMClient 实例— 偏好提取和文章编辑用同一个 LLM 配置,但创建独立实例,避免状态污染。

完整闭环

第1次:用户"太正式了,活泼点" → 修改文章(V1→V2) → 提取偏好"语气:活泼" → 写入人设 第2次:生成新文章 → 自动注入"语气:活泼" → 直接活泼风格(不用用户再说) 第3次:用户"emoji少一点" → 修改文章(V3→V4) → 提取偏好"emoji:低频" → 写入人设 第4次:生成新文章 → 自动注入"语气:活泼 + emoji:低频" → 风格越来越精准

对话修改 + 风格进化 = 越用越懂你。用户每改一次,Agent 就学一点,下次生成更好。这不是两个独立功能,是同一个闭环的两面。

七、错误处理:修改失败怎么办?

LLM 调用可能失败 — 网络超时、API 限流、输出格式异常。修改流程需要优雅降级。

修改失败的降级

if req.regenerate: try: revised_body, revised_title = await _regenerate_with_suggestion(...) # ... 正常流程 except Exception as e: logger.error(f"重新生成失败: {e}") ai_msg = ChatMessage( session_id=session.id, role=MessageRole.ASSISTANT, content=f"抱歉,根据建议重新生成时出错:{e}。请尝试换一种表述方式。", )

修改失败时

  • 不抛异常给前端(用户看不懂 traceback)

  • 返回友好的错误消息("请尝试换一种表述方式")

  • 用户消息已保存(不会丢失建议)

  • 内容不更新(保持原文不变)

偏好提取失败的降级

try: new_prefs = await learner.extract_preference_from_revision(revision) # ... 写入人设 except Exception as e: logger.warning(f"即时偏好提取失败(不影响主流程): {e}")

偏好提取失败时

  • 只记 warning 日志

  • 不影响修改结果(文章已经改好了)

  • 不影响内容更新(用户已经看到修改后的文章)

  • 下次修改再尝试提取

两层降级,各保各的— 修改是主流程,必须保;偏好提取是副流程,可以丢。主副分离,互不拖累。

踩坑总结

根因修复
修改后原文丢失只存修改后的文章加了RevisionRecord,同时存original_bodyrevised_body
版本号不直观用"修改#1、修改#2"改为"V1→V2、V2→V3",有方向感
重新生成风格漂移Prompt 说"重新生成"改为"根据建议调整文章,只修改建议的部分"
修改失败丢建议异常中断整个请求用户消息先保存,修改失败只影响 AI 回复
偏好提取失败影响修改没有隔离主副流程try/except隔离,偏好提取失败不影响修改
界面卡顿悲观更新,等 AI 回复才显示乐观更新,先显示用户消息
AI 回复太长刷屏返回完整修改后文章只返回前 200 字预览,revised_body[:200]
对话和修改记录混存都放在 messages 里分开存:messages存对话文本,revisions存完整记录
修改后不进化只改了文章没提取偏好修改后自动调extract_preference_from_revision

经验总结

  1. 对话修改的核心是"编辑"不是"生成"— Prompt 里"只修改用户建议的部分"这句话,决定了 LLM 是在原文基础上调整,而不是从零写一篇新的

  2. 三个模型各司其职ChatMessage存轻量对话文本,RevisionRecord存重量完整记录,ChatSession是聚合根,按需加载互不拖累

  3. 版本链是修改的"前世今生"— 每条RevisionRecord记录相邻版本的 diff,串起来就是完整修改历史,支持回退、偏好提取、审计追溯

  4. 乐观更新让体验流畅— 先显示用户消息再等 AI 回复,5-10 秒的处理时间用户不会觉得卡顿

  5. 主副流程隔离— 修改是主流程必须保,偏好提取是副流程可以丢,try/except隔离,互不拖累

  6. 对话修改 + 风格进化 = 越用越懂你— 每次修改触发偏好提取,写入人设,下次生成自动注入,形成闭环

下篇预告

下一篇讲质检系统:Agent的自我审查— 质检是 Agent 的"良知",不自检的 Agent 就像没有编辑的报社。多维度质检(口语化、去重、逻辑检查、敏感词)+ orchestrator 编排 + 分数阈值 + 质检与生成的闭环。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/26 1:26:46

Java RAG系统架构分层设计与SSE流式响应实战

1. 项目缘起:从单体混沌到分层清晰的RAG系统演进去年年底,我们团队接手了一个内部知识库问答系统的重构任务。最初的版本是一个典型的“赶工”产物:所有的代码——从PDF解析、文本切片、向量化嵌入,到最后的检索与大模型生成——都…

作者头像 李华
网站建设 2026/8/26 1:22:40

边缘AI视觉部署选型指南:5大芯片平台×6大推理引擎全解析

边缘 AI 视觉部署选型指南:5 大芯片平台 6 大推理引擎全解析 文章目录 边缘 AI 视觉部署选型指南:5 大芯片平台 6 大推理引擎全解析前言一、5 大芯片平台概览二、6 大推理引擎详解2.1 RKNPU(瑞芯微)2.2 CANN(华为昇腾…

作者头像 李华
网站建设 2026/8/25 23:55:31

FlinkSQL 处理 binlog:changelog 的三种处理方式

「我的数据空间」实时计算实践笔记 Flink SQL 系列使用 Flink 临时表 使用 DDL 声明 对应的 schema 和 format CREATE TABLE KafkaSource (id VARCHAR,count BIGINT,changelog BOOLEAN ) with (topictopic_ods_order_event,connectorkafka,formatbinlog,binlog.with-changelo…

作者头像 李华
网站建设 2026/8/25 23:46:17

AI智能体系统安全防御:从间接提示词注入到架构级防护

1. 从一次真实的“安全策略拦截”事件谈起最近在调试一个多智能体系统时,遇到了一个让我印象深刻的报错。系统日志里赫然写着:ef1 usb device news disk hans been blocked by zhe current security policy。这行看似语法混乱、充满拼写错误的英文&#…

作者头像 李华