跳到主要内容

聊天循环(默认能力)

这章讲什么: 默认 chat 能力的循环。它和 01 章的通用标签循环是两套并行实现——chat 用 OpenAI 原生 tool-calling,不靠文本标签区分动作,而是靠「这一轮有没有 tool_calls」。读这章看 DeepTutor 怎么把「一次回合」做成一条干净的单循环。

1. 它要解决的小问题

传统 RAG 聊天常是两段式:先「探索/检索」一遍,再「生成回答」一遍。问题是:到底要不要探索?探索几次?哪段文字是给用户的答案、哪段只是中间叙述? 两段式要在中途猜这些,容易把中间产物泄进答案,或多跑无谓的检索。

2. 思路 / 直觉:一次对话一个循环,用「调没调工具」自然分界

DeepTutor 把一个聊天回合做成一条增长的对话上的单一 agent 循环(deeptutor/agents/chat/agent_loop.py:1-23 docstring):

  • 每一轮 = 一次 LLM 调用,它的文字实时流给用户
  • 调了工具的那轮 = 叙述(narration):文字是工具工作的前言,循环继续。
  • 没调工具的那轮 = 收尾(finish):它的文字就是最终答案,循环结束。(第一轮就不调工具 = 「无需探索」的快路径。)
  • 预算耗尽还在要工具,就强制再跑一轮「无工具的收尾」。

关键好处(docstring 原话):没有单独的 respond 阶段,也不用在流到一半时猜文字该去哪——每轮文字都实时流给用户,轮末发一个 call_role(narration vs finish)告诉前端怎么渲染这轮文字(agent_loop.py:565-579)。

3. 图示:聊天回合的一轮

怎么读: 核心分叉只有一个——「这一轮 result 里有没有 tool_calls」。

一轮开始


_call_llm:一次流式 LLM 调用(带 tools=schema, tool_choice=auto)
│ 文字实时 stream.content;<think> 段被 InlineThinkFilter 切去思考侧栏

有 tool_calls 吗?

├── 没有 ──► 这轮文字 = 最终答案 → _finalize_finish → 结束
│ (若文字为空且没 nudge 过 → 提醒模型「别只想不做」再来一轮)

└── 有 ───► 这轮是叙述
├ 把 assistant(文字+tool_calls) 入对话
├ _dispatch_tool_calls 并行执行,role=tool 结果入对话
├ pause(ask_user)? → 等用户回复,替换进 tool 消息,继续
├ 折叠「上下文检查点」(把长上下文压成一条 system 摘要)
├ terminate? → 发终结产物,结束
└ 否则 → 下一轮

主体在 agent_loop.py:219,AgentLoop._run_loop

4. 核心机制

4.1 InlineThinkFilter:流式切 <think> 标签

有的 provider 把推理用 <think>...</think> 内联在 content 通道里(而不是 reasoning_content)。InlineThinkFilter(agent_loop.py:57)在流式时就把它切开:think 内的去思考侧栏,think 外的进用户答案区。

精妙处:它会回退查找未闭合的尾标签——如果 buffer 末尾出现一个还没收全的 </thinki,就先扣住最多 24 字(_TAG_HOLDBACK_CHARS)等下一个 chunk,避免把半截标签当正文吐出去(agent_loop.py:87-94)。而喂回 LLM 的原始文本是带标签不动的——只是用户看到的通道干净了。

# 示意,非源码:增量喂 chunk,吐出 (kind, text) 段,kind ∈ {content, thinking}
filt = InlineThinkFilter()
async for chunk in llm_stream:
for kind, seg in filt.feed(chunk.content): # 可能扣住半截尾标签到下次
if kind == "content":
await stream.content(seg) # 进用户答案气泡
else:
await stream.thinking(seg) # 进思考侧栏
for kind, seg in filt.flush(): # 流结束,放出扣住的残余
...

4.2 「只想不做」的一次性 nudge

如果某轮没调工具、清洗后文字又是空的(模型把整段回复都塞进 <think> 里,只规划没动手),循环直接给个空答案,而是把原始文字留在对话里、注入一条「继续:要么调工具执行你的计划,要么直接写最终答案」的提示,再给它一次机会(agent_loop.py:261-297,nudged_empty_finish)。

4.3 强制收尾的双重容错

  • 预算耗尽:还在要工具就跑一轮禁用工具的收尾,逼模型作答(agent_loop.py:364,_forced_finish,tool_schemas=None)。
  • 中途 LLM 失败:不是第一轮的话,不丢弃已收集的工作——降级走强制收尾(agent_loop.py:245-257)。只有第一轮(啥都没攒到)才向上抛异常。
  • 收尾也产不出文字:发一个兜底答案,不让回合空手而归(agent_loop.py:406,_finalize_finish)。

4.4 上下文检查点折叠

工具结果可能很长。某些工具会在 metadata 里带一个 _context_checkpoint 摘要;循环把它折叠成一条 [Context checkpoint] 的 system 消息,截断掉检查点之前的冗长尾巴,把对话压短再继续(agent_loop.py:344,_fold_context_checkpoint;摘要提取见 _last_context_checkpoint_summary)。

5. 它怎么接进通用循环 / 提示怎么组装

聊天能力的流水线 AgenticChatPipeline(deeptutor/agents/chat/agentic_pipeline.py:181)负责:

  • run(agentic_pipeline.py:301):准备延迟工具、判断 exec 是否允许、组装启用的工具集 + OpenAI tool schema,然后起一个 AgentLoop 跑(agentic_pipeline.py:332-342)。
  • _build_loop_messages(agentic_pipeline.py:345):把系统提示 + 工具清单 + 知识库种子块 + 能力专属块拼成初始消息。
  • effective_max_rounds(agentic_pipeline.py:267):回合的轮预算,会被能力声明的 _min_loop_rounds 抬高(比如 subagent 能力需要保证足够的咨询预算)——一个保持循环能力无关的通用缝(agentic_pipeline.py:267-280)。

注意:deeptutor/agents/chat/chat_agent.py 里还有一个更轻量的旧 ChatAgent(基于 token 截断的多轮对话),但当前默认聊天走的是上面这条 AgenticChatPipeline + AgentLoop 的单循环路径(README 的 v1.4 系列把 chat「moved to a single agent loop」)。

6. 边界与局限

  • chat 这套依赖 provider 支持原生 tool-calling;不支持时 _create_response_stream 探测到错误后会去掉 tools 重试(agent_loop.py:582),退化成无工具的纯对话——这时「调没调工具」的分界自然消失,只能靠 finish。
  • ask_user 的 pause 让回合挂起;若用户不回复或放弃,回合以「未完成」收场,pending 问题就是最终产物(agent_loop.py:313-325)。

7. 横向对比

这是 DeepTutor 内部「两种循环范式」的对照:

通用标签循环(01 章)聊天循环(本章)
动作怎么定文本标签(THINK/TOOL/FINISH)OpenAI 原生 tool_calls 的有无
适用solve / research / 各深度能力默认 chat / mastery
收尾判定命中 terminal 标签某轮没有 tool_calls
推理分流run_labeled_step_emit_textInlineThinkFilter + reasoning_content

两者都把「推理 / 工具 / 答案」分流到不同 StreamBus 事件,只是触发信号不同。

8. 代码地图

主题文件符号
聊天单循环deeptutor/agents/chat/agent_loop.pyAgentLoop_run_loop_call_llm_forced_finish_finalize_finish
流式切 think 标签deeptutor/agents/chat/agent_loop.pyInlineThinkFilter(feed/flush)
上下文检查点折叠deeptutor/agents/chat/agent_loop.py_fold_context_checkpoint_last_context_checkpoint_summary
流水线 / 提示组装deeptutor/agents/chat/agentic_pipeline.pyAgenticChatPipelinerun_build_loop_messageseffective_max_rounds
旧轻量聊天 agentdeeptutor/agents/chat/chat_agent.pyChatAgent