用FunctionCall实现文件解析(七):被动地等ChatOpenAI塞进来

前言

上一篇文章中,我们研究了怎么样主动把ChatOpenAI塞进去,那么接下来就是得研究怎么把ChatOpenAI被动地塞进来。

触发机制

与上一篇文章的ChatRender不同的是,如果被动地塞进来,那就不可以使用这种主动的类,而必须采用invoke或者stream相关的callback过程。

这是个什么过程呢?

stream或者invoke方法中,如果我们配置了config字典中的callback属性,他就会开启CallbackManager,然后创建一个你传入的YourCallbackHandler实例。这个YourCallbackHandler继承自BaseCallbackHandler,他虽然父类特别多,但是也有一个核心的东西:CallbackManagerMixin。这个类中就定义了on_llm_start等钩子函数。

那也就是说,如果我们继承BaseCallbackHandler类,然后定义了其中on_llm_new_token或者on_llm_end钩子,就会自然而然地触发我们自定义的逻辑。比如说,我们定义on_llm_new_token中检测到大模型输出爆炸就真的把电脑炸到天花板上,当invoke或者stream执行的时候,电脑就开始准备发光发热了(物理)。

实现代码

由于大致的逻辑跟上一篇文章大差不差,所以就不再多做什么解释了。

我们直接试试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class ChatRenderCallbackHandler(BaseCallbackHandler):
"""把 LLM 流式 token 渲染到 Streamlit,双容器分流。"""
def __init__(
self,
role: str = "assistant",
save: bool = True,
label: str = "🤔 思考过程",
):
super().__init__()
self.role = role
self.save = save
self.label = label

# UI 占位:只有 answer 先占位,<think> 动态生成
with st.chat_message(role):
self.answer_holder = st.empty()

self.expander = None
self.think_holder = None

# 累积完整文本
self.buffer = ""

# ===== Callback API =====
def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
"""收到新 token 就增量刷新 UI。"""
self._update(token)

def on_llm_end(self, response, **kwargs: Any) -> None:
"""流式结束,把完整内容写进 session_state。"""
if self.save:
st.session_state.dashscope["messages"].append(
{"role": self.role, "content": self.buffer}
)

# ===== 更新 =====
def _update(self, delta: str) -> None:
if not delta: return
self.buffer += delta

# 拆分 <think>
think_parts = "\n".join(_THINK_RE.findall(self.buffer))
answer_text = _THINK_RE.sub("", self.buffer).strip() or " "

# 第一次检测到 <think> ⇒ 创建 expander
if think_parts and self.expander is None:
self.expander = st.expander(self.label, expanded=False)
self.think_holder = self.expander.empty()

# 刷新 UI
if self.think_holder is not None:
self.think_holder.markdown(think_parts)
self.answer_holder.markdown(answer_text)

使用

既然都完成了,那就开始用吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from utils.chat_render import ChatRenderCallbackHandler
if prompt := st.chat_input("你得注意你的言行……求你了……(´;ω;`)"):
st.session_state.dashscope["messages"].append(
{'role': 'user', 'content': prompt}
)
st.chat_message('user').markdown(prompt)
_ = list(
llm.stream(
[HumanMessage(content = prompt)],
config = {"callbacks": [
ChatRenderCallbackHandler(role = "assistant")
]}
)
)

然后,就有效果了:

效果呈现

其实本质上是和上一篇文章的效果是一模一样的。