Skip to content

解析和渲染

发布于:编辑此页

输出的“多态”

从 2022年 12月 ChatGPT 推出后到现在,大模型现在拆分成了 通用型模型推理型模型

简单的 “聊天” 已经无法满足这几年延伸出来的需求。

我们更期望大模型能做更多的事情,客户端能渲染更多的内容,例如:

输出的解析和渲染

如果你之前做过电商 ToC 的业务,大模型输出的解析和渲染,你会感觉有点像 首页的装修

运行 ollama run deepseek-r1:32b 并给出提词 1+1等于几?

llm-output

由于 deepseek-r1推理型模型,它按照 <think>{思考过程}</think>{回答} 做出了返回。

大模型返回纯文本时,整个输出就是一段 Markdown

大模型输出的解析和渲染就是 Markdown 的解析和渲染。

Markdown 解析为 HTML 并渲染到页面,这不是多年前静态网站在干的事吗?

到现在有非常多的库可以完成这种转换,生态也非常丰富,上面列举的这些库都是我曾使用过的,

如果你使用 React,也可以直接使用 react-markdown

我们只需要在解析 Markdown 时为不同的 language- 匹配不同的组件,例如下方的样板代码:

Message.tsx
import { useState, useMemo, memo, useEffect } from "react";
import ReactMarkdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { dark } from "react-syntax-highlighter/dist/esm/styles/prism";
import RemarkMath from "remark-math";
import RemarkBreaks from "remark-breaks";
import RehypeKatex from "rehype-katex";
import "katex/dist/katex.min.css";
 
const CollapseReference = data => <LLMCollapseReference data={data} />;
const CollapseThink = data => <LLMCollapseThink data={data} />;
const ToolWeather = data => <LLMToolWeather data={data} />;
const ToolAmap = data => <LLMToolAMap data={data} />;
const ToolBi = data => <LLMToolBi data={data} />;
 
const Message = memo(({ message }) => {
  const components = {
    code: ({ className, children, ...rest }) => {
      const match = /language-(\w+)/.exec(className || "");
      const lang = match && match[1];
 
      if (lang) {
        switch (lang) {
          case "reference":
            return <CollapseReference data={children} />;
 
          case "think":
            return <CollapseThink data={children} />;
 
          case "tool-weather":
            return <ToolBi data={JSON.parse(children)} />;
 
          case "tool-amap":
            return <ToolAmap data={children} />;
 
          case "tool-bi":
            return <ToolWeather data={JSON.parse(children)} />;
 
          default:
            return (
              <SyntaxHighlighter
                {...rest}
                PreTag="div"
                children={String(children).replace(/\n$/, "")}
                language={match[1]}
                style={dark}
              />
            );
        }
      }
 
      return <code className={`language-${lang}`}>{children}</code>;
    },
  };
 
  const remarkPlugins = [RemarkMath, RemarkBreaks];
 
  const rehypePlugins = [RehypeKatex];
 
  return (
    <ReactMarkdown
      children={message}
      components={components}
      remarkPlugins={remarkPlugins}
      rehypePlugins={rehypePlugins}
    />
  );
});
 
export default Message;

样板代码中用到了一些 remarkPluginsrehypePlugins

如果你不清楚它们是干什么的,可以浏览我之前写的 unified

标签的状态机

通过不同的 自定义标签 来渲染不同的 自定义组件 仅仅只是将输出展示到了页面,

如果客户端做一些动态变化的交互,就需要解析出标签的状态机:

上面最常见的两个例子分别对应了:标签开始 -> 标签未闭合 -> 标签闭合

{ role: 'assistant', content: '', refusal: null }
/** 标签开始 **/
{ content: '<think>' }
/** 标签未闭合 **/
{ content: " don't" }
/** 标签未闭合 **/
{ content: ' scientists' }
/** 标签未闭合 **/
{ content: ' trust' }
/** 标签未闭合 **/
{ content: ' atoms' }
/** 标签未闭合 **/
{ content: '?\n\n' }
/** 标签未闭合 **/
{ 标签未闭合: 'Because' }
/** 标签未闭合 **/
{ content: ' they' }
/** 标签未闭合 **/
{ content: ' make' }
/** 标签未闭合 **/
{ content: ' up' }
/** 标签未闭合 **/
{ content: ' everything' }
/** 标签闭合 **/
{ content: '!</think>' }
{}

可以参考 page-assist 中的工具函数来实现状态机的解析,

如果你是个“伸手党”,我更推荐直接使用 parseReasoning

const renderMessage = () => {
  let completionContent: string = "";
 
  let referenceContent: string = "";
  let referencing: boolean = false;
 
  let thinkContent: string = "";
  let thinking: boolean = false;
 
  // 提取 reference
  parseReasoning(content, "reference").map(item => {
    if (item.type === "reasoning") {
      referenceContent = item.content;
      referencing = item.reasoning_running;
    } else if (item.type === "text") {
      completionContent = item.content;
      referencing = item.reasoning_running;
    }
  });
 
  // 提取 think
  if (!referencing) {
    parseReasoning(completionContent, "think").map(item => {
      if (item.type === "reasoning") {
        thinkContent = item.content;
        thinking = item.reasoning_running;
      } else if (item.type === "text") {
        completionContent = item.content;
        thinking = item.reasoning_running;
      }
    });
  }
 
  return (
    <Typography>
      {referenceContent || referencing ? (
        <CollapseReference content={referenceContent} loading={referencing} />
      ) : null}
 
      {thinkContent || thinking ? (
        <CollapseThink content={thinkContent} loading={thinking} />
      ) : null}
 
      {!referencing && !thinking ? (
        <Message message={completionContent} />
      ) : null}
    </Typography>
  );
};

一个大模型输出的解析、渲染、自定义标签的状态机 样板代码 基本就完成了。


上一篇
交互优化
下一篇
SSE