はじめに
ChatGPTやClaudeを使ったアプリを開発していると、ひとつ厄介な問題に直面するんですよ。
AIがMarkdown形式でレスポンスを返してくるんですが、ストリーミング表示の途中で「**太字が閉じてない」とか「```コードブロックが中途半端」みたいな状態になる。これ、そのままレンダリングすると表示が崩れまくるんですよね。
で、この問題を綺麗に解決してくれるのが今回紹介する「Remend」というライブラリです。
Remendとは
RemendはVercel社が開発した、いわば「自己修復するMarkdownパーサー」です。不完全なMarkdown構文を検出して、自動的に閉じタグを補完してくれます。
個人的には、AIストリーミング対応のアプリを作るなら、これ一択ですね。
特徴・メリット
1. ストリーミングに最適化
トークン単位で生成されるAIレスポンスに対応。途中経過でも常に正しいMarkdownとして表示できます。
2. スマートな自動補完
以下の構文を自動でクローズしてくれます。
**太字**-**text→**text***イタリック*-*text→*text*`インラインコード`-`code→`code`[リンク](URL)- 不完全なリンクも適切に処理$$数式$$- LaTeX数式ブロック~~取り消し線~~
3. 高パフォーマンス
正規表現を使わない文字列操作で実装されているので、メモリ効率がいい。ストリーミングで頻繁に呼び出しても負荷が少ないのは大きなメリットです。
4. ゼロ依存
純粋なTypeScript実装で依存関係がありません。バンドルサイズを気にする人には嬉しいポイント。
5. コンテキスト認識
コードブロック内や数式ブロック内では、余計な補完をしない賢さがあります。これ、意外と重要なんですよね。
インストール方法
npmでサクッと入ります。
npm install remend
yarnやpnpmでも問題なし。
yarn add remend
# または
pnpm add remend
基本的な使い方
使い方はシンプルで、Markdownをパースする前にremend関数を通すだけです。
import remend from "remend";
// AIからのストリーミングレスポンス(途中経過)
const partialMarkdown = "これは**太字のテキスト";
// Remendで補完
const completed = remend(partialMarkdown);
// 結果: "これは**太字のテキスト**"
Reactでの実装例
実際のチャットアプリでの使用イメージはこんな感じ。
import { useState, useEffect } from 'react';
import remend from 'remend';
import ReactMarkdown from 'react-markdown';
function StreamingMessage({ stream }: { stream: AsyncIterable<string> }) {
const [content, setContent] = useState('');
useEffect(() => {
let buffer = '';
(async () => {
for await (const chunk of stream) {
buffer += chunk;
// ストリーミング中は常にremendで補完
setContent(remend(buffer));
}
// 完了後は生のMarkdownをセット
setContent(buffer);
})();
}, [stream]);
return <ReactMarkdown>{content}</ReactMarkdown>;
}
remark/unifiedとの連携
remark系のパイプラインと組み合わせる場合は、パース前に処理するのがポイントです。
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import remend from 'remend';
async function processMarkdown(input: string, isStreaming: boolean) {
// ストリーミング中のみremendを適用
const markdown = isStreaming ? remend(input) : input;
const result = await unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeStringify)
.process(markdown);
return String(result);
}
実践的なユースケース
AIチャットアプリ
正直なところ、これが一番メインの使い道です。ChatGPTライクなインターフェースを作るとき、ストリーミング表示の品質が格段に上がります。
// OpenAI APIとの組み合わせ例
import OpenAI from 'openai';
import remend from 'remend';
const openai = new OpenAI();
async function* streamChat(message: string) {
const stream = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: message }],
stream: true,
});
let buffer = '';
for await (const chunk of stream) {
buffer += chunk.choices[0]?.delta?.content || '';
yield remend(buffer);
}
}
ドキュメント生成ツール
AIにドキュメントを書かせるツールでも活躍します。生成途中のプレビューが崩れないので、UXが良くなる。
リアルタイムエディタ
Markdownエディタで、入力中のプレビューを表示する場合にも使えます。ユーザーが構文を打ち終わる前からプレビューが綺麗に表示される。
Streamdownとの関係
ちなみにRemendは、同じくVercelが開発している「Streamdown」というライブラリの内部で使われています。Streamdownはreact-markdownの完全な代替を目指したライブラリで、AIストリーミング対応が組み込まれています。
もしReactでがっつりMarkdown表示をするなら、Streamdownを使うのもアリ。Remendは単体で使いたいとき用という位置づけですね。
# Streamdownを使う場合
npm install streamdown
import { Streamdown } from 'streamdown';
function MarkdownViewer({ content }: { content: string }) {
return <Streamdown>{content}</Streamdown>;
}
まとめ
Remendは、AIストリーミング時代において地味だけど確実に必要になるライブラリです。
- 不完全なMarkdownを自動補完
- ゼロ依存で軽量
- TypeScript製で型安全
30代になって思うのは、こういう「痒いところに手が届く」系のライブラリって本当にありがたいんですよね。自分で実装しようとすると意外とエッジケースが多くて面倒なので。
AIチャットアプリを開発している人は、ぜひ導入を検討してみてください。ストリーミング表示のQOLが確実に上がります。
