聊天循环(默认能力)
这章讲什么: 默认
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_text | InlineThinkFilter + reasoning_content |
两者都把「推理 / 工具 / 答案」分流到不同 StreamBus 事件,只是触发信号不同。
8. 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 聊天单循环 | deeptutor/agents/chat/agent_loop.py | AgentLoop、_run_loop、_call_llm、_forced_finish、_finalize_finish |
| 流式切 think 标签 | deeptutor/agents/chat/agent_loop.py | InlineThinkFilter(feed/flush) |
| 上下文检查点折叠 | deeptutor/agents/chat/agent_loop.py | _fold_context_checkpoint、_last_context_checkpoint_summary |
| 流水线 / 提示组装 | deeptutor/agents/chat/agentic_pipeline.py | AgenticChatPipeline、run、_build_loop_messages、effective_max_rounds |
| 旧轻量聊天 agent | deeptutor/agents/chat/chat_agent.py | ChatAgent |