0. 前言#
プロジェクトは Github でオープンソース化されており、ここではいくつかの基本的な技術説明があります。
1. 構造設計:プラグイン化プロジェクト#
プロジェクトでは、プラグイン化アーキテクチャを採用してシステムを構築し、Ncatbot のプラグインモードを利用して、メインプログラムと機能をうまく分離しています。
プラグインアーキテクチャの利点は、モジュールの境界が明確で、依存関係の隔離度が高く、システムの拡張性が強いことです。標準的なプラグイン化は、コード間の結合を大幅に解消し、開発やデプロイが非常に友好的になります。
プロジェクトのシステムディレクトリ構造は以下の通りです:
ModuleChat/
├── main.py # プラグインのメインエントリ、コマンド登録とスケジューリングロジックを担当
├── chat.py # モデル適応層、ローカルおよびクラウドモデルの呼び出しをカプセル化
├── config.yml # 設定ファイル、モデルパラメータと有効オプションを集中管理
├── requirements.txt # 依存ライブラリ
└── cache/
└── history.json # チャット履歴メモリファイル
chat.py はシステムのコアモジュールで、メインプログラム main.py はコマンドを受信、解析し、メッセージをモデルモジュールにルーティングします。開発においてこれは非常に友好的な構造であり、特定の機能モジュールの開発とデバッグに集中でき、メンテナンスの複雑さを大幅に低下させます。
設定項目は config.yml に集中しており、柔軟性と環境適応能力をさらに向上させています。
一時的な json ファイルの方式を採用し、コマンドの呼び出しと返信を記録し、API インターフェースに渡すことで、大モデルが一定程度の短期記憶機能を得ることができますが、ある程度においてこれはシステムにとって友好的ではありません。私は、データベースの形式を採用する方がより良い解決策だと考えていますが、データベースを使用するとシステムの複雑性が大幅に増すため、json ファイルの使用は非常に良い代替選択です。
2. プラグインメインプログラム:コマンドのデカップリングとルーティング中枢#
main.py はプラグインのメインエントリで、register_user_func メソッドを通じて二つのコマンド /chat と /clear chat_history を登録します。これらはそれぞれチャット機能と履歴のクリアに対応しています。
さらに、メインプログラムは画像メッセージの自動認識をサポートし、画像 URL を抽出して chat_model_instance.recognize_image メソッドに渡し、視覚的な説明を自動的に取得します。
if image_url and self.chat_model.get('enable_vision', True) and not self.chat_model.get('use_local_model'):
# 画像認識機能を使用
image_description = await chat_model_instance.recognize_image(image_url)
user_input = f"ユーザーが画像を送信しました。画像の説明は:{image_description}。ユーザーは言いました:{user_input}"
elif image_url and not self.chat_model.get('enable_vision', True):
# 画像認識機能は無効ですが、ローカルモデルかどうかを確認
if self.chat_model.get('use_local_model'):
user_input = f"ユーザーが画像を送信しましたが、ユーザーはローカルモデルを使用しているため、画像認識はできません。ユーザーは言いました:{user_input}"
else:
user_input = f"ユーザーが画像を送信しましたが、画像認識機能は無効です。ユーザーは言いました:{user_input}"
視覚的な説明を取得した後、言語大モデルに出力を行うのは、実際には非常に良い解決策です。現在の使用シーンでは、画像を認識した後に内容を分析処理する必要が多く、画像自体を処理するのではありません。これにより、API 呼び出しを大幅に減少させ、キャッシュのヒット率を向上させ、TOKEN の使用を減らし、API 呼び出しコストを低下させることができます。また、クラウドの大モデルを使用して認識し、その後ローカルの大モデルに回答を渡すことで、コストをさらに圧縮できます。
エラーハンドリングに関しては、try...except がチャットロジック全体を包み込み、画像デコードの失敗や API 異常によるメインフローの崩壊を防ぎ、プラグインの堅牢性を保っています。
統合的に見ると、main.py は典型的な「ライトコントローラー」パターンであり、各コンポーネントを調整するだけでビジネスロジックの詳細を担わず、全体のプラグインに良好なエンジニアリングの可読性を持たせています。
3. モデル適応モジュール:複数モデルのカプセル化とセマンティック一貫性#
chat.py はプラグインのコアロジックです。モデルの呼び出し、チャット履歴の記憶、画像認識などのタスクを処理します。複数のモデルインターフェース(OpenAI API と Ollama ローカルサービスなど)に対応するため、統一されたカプセル化インターフェースの戦略を採用し、外部の呼び出し者がモデルの詳細を気にせず、useCloudModel() または useLocalModel() の二つのメソッドを通じて対話を完了できるようにしています。
async def useLocalModel(self, msg: BaseMessage, user_input: str):
"""ローカルモデルを使用してメッセージを処理"""
try:
# メッセージリストを構築し、履歴を含める
messages = self._build_messages(user_input, msg.user_id if hasattr(msg, 'user_id') else None)
response: ChatResponse = chat(
model=self.config['model'],
messages=messages
)
reply = response.message.content.strip()
# 現在の対話を履歴に保存
if hasattr(msg, 'user_id'):
self._update_user_history(msg.user_id, {"role": "user", "content": user_input})
self._update_user_history(msg.user_id, {"role": "assistant", "content": reply})
except Exception as e:
reply = f"リクエスト中にエラーが発生しました:{str(e)}"
return reply
注目すべきは、OpenAI インターフェースの呼び出しと Ollama の呼び出しには若干の違いがあり、特定のモデルのパラメータが完全ではないため、OpenAI インターフェースを使用することで、クラウドモデルを呼び出すと実際にはより良い体験を得られることです。たとえば、モデルの temperature を制御して、より想像力豊かにしたり、リアルタイムに重点を置いたりして、幻覚を減らすことができます。
すべてのユーザーの履歴は cache/history.json ファイルに保存されており、これは持続的な保存のソリューションであり、一定程度の追跡可能性を持っています。履歴は _update_user_history メソッドを通じて動的に更新され、設定ファイルで定義された最大ラウンド数内に制御されます。この方法は、コンテキストが大きすぎることによるパフォーマンスの問題を防ぎ、モデルが連続したコンテキストを理解できるようにし、回答の質を向上させ、インターフェースを接続しても近似的な記憶能力を持つことができます。
クラスには OpenAI の画像認識モデルも統合されており、_build_vision_messages を通じてマルチモーダルメッセージ構造を構築します。また、設計上、画像処理、メッセージ構築、例外処理、モデル呼び出しなどの関数を分離しており、開発中に問題の所在をより早く特定でき、オープンソース化後も他の開発者が読みやすくなっています。
4. クラウドモデルの接続(OpenAI):標準化されたカプセル化#
クラウドモデルの呼び出しは主に openai 公式ライブラリを通じてカプセル化され、chat.completions.create メソッドを利用してコンテキストの構築と返信の生成を行います。毎回の呼び出しで _build_messages() メソッドを通じて完全な対話コンテキストを構築し、システムプロンプトと cache/history.json に保存された履歴を追加し、多ラウンドの記憶式対話を実現します。
def _build_messages(self, user_input: str, user_id: str = None):
"""メッセージリストを構築"""
messages = []
# システムプロンプトを追加
system_prompt = self.config.get('system_prompt', "あなたはチャットパートナーのロボットです")
messages.append({"role": "system", "content": system_prompt})
if user_id:
history = self._get_user_history(user_id)
messages.extend(history)
# 現在のユーザー入力を追加
messages.append({"role": "user", "content": user_input})
return messages
呼び出しロジックには temperature パラメータがカプセル化されており、設定ファイルを通じてモデル出力のランダム性を柔軟に制御できます。
バグレポートに遭遇した場合、統一して return を使用してエラーをユーザーにフィードバックします。これにより、大量のエラーハンドリングを減らし、設定ミスによる一般的な問題をより明確にフィードバックできます。つまり、統一モデル + ルール処理の二つの方法で実行時に遭遇した問題をフィードバックします。
if "401" in str(fallback_error) or "Unauthorized" in str(fallback_error):
raise Exception("モデルAPI認証に失敗しました。設定ファイルを確認してください。")
raise Exception(f"画像認識中にエラーが発生しました: {str(e)}, 代替方法も失敗しました: {str(fallback_error)}")
結果が返された後、今回の問答はユーザーの履歴キャッシュに同期され、ローカルファイルに保存され、次回の対話で正常にコンテキストを取得できるようになります。これにより、メモリ依存を減らし、キャッシュのヒット率を向上させ、後続のデバッグや行動の再現に基づく証拠を提供します。
5. ローカルモデルの呼び出し(Ollama):軽量化推論とインターフェースの統一#
ローカルモデルの呼び出しは ollama.chat() を通じて行われ、_build_messages() のコンテキスト構築ロジックを再利用して、呼び出しロジックとクラウドの一貫性を保ち、インターフェースの一貫性を維持します。
このローカル推論メカニズムの利点は非常に明確です:ネットワークがない場合やプライベートデプロイ環境でもインテリジェントな対話機能を使用でき、プラグインのデプロイの柔軟性と安全性を大幅に向上させます。プライバシーに敏感なシーンでも、ローカルでのデプロイと実行が可能です。
設計上、ローカルとクラウドの呼び出しインターフェースを一致させ(どちらも use*Model() にカプセル化)、外部の呼び出し側がモデルの出所を判断する必要がなく、複雑さを減少させます。さらに、履歴の更新や例外捕捉メカニズムも実装されており、ローカルモデルはクラウドと同じ機能の完全性と安定性を持っています。
6. 画像認識処理ロジック:マルチモーダル入力のセマンティック強化戦略#
画像認識機能はこのプラグインの大きな特徴です。プラグインは画像メッセージを認識し、OpenAI の視覚モデルを通じて処理することをサポートしています。全体のプロセスは以下の通りです:
-
- 画像メッセージから URL を抽出;
for segment in msg.message:
if isinstance(segment, dict) and segment.get("type") == "image":
image_url = segment.get("data", {}).get("url")
break
-
- HTTP リクエストを通じて画像内容を取得し、Base64 エンコード;
-
- 視覚入力フォーマットを構築(
image_urlとtext promptを含む);
- 視覚入力フォーマットを構築(
response = requests.get(image_url)
response.raise_for_status()
return base64.b64encode(response.content).decode('utf-8')
-
- 視覚モデルを呼び出して画像の説明を完了;
# 画像を取得してエンコード
image_data = self._encode_image_from_url(image_url)
# メッセージを構築
messages = self._build_vision_messages(image_data, prompt)
# 視覚モデルを呼び出す
response = self.vision_client.chat.completions.create(
model=self.config.get('vision_model'),
messages=messages,
temperature=self.config.get('model_temperature', 0.6),
stream=False,
max_tokens=2048
)
-
- 画像の説明をユーザー入力に結合し、コンテキストのセマンティック完全性を向上させます。
このメカニズムは、画像とテキストが混在する入力シーンにおける情報の非対称性の問題を効果的に解決し、呼び出しを段階的に調整することで、クラウドの高い計算能力を利用して複雑な問題を処理し、その後ローカルで簡素化された問題を処理することで、TOKEN の使用率を大幅に減少させます。
エラーハンドリングに関しては、二段階のフォールバック戦略を設計しました。主な呼び出しが失敗した場合は、プレーンテキストのフォールバックプロンプトを試み、まだ失敗する場合は、ユーザーに API キーやモデルの状態を確認するように促します。このようなフォールトトレランス設計により、プラグインは部分的な失敗時でもサービスを中断せずに維持できます。
7. チャット履歴システム:記憶ウィンドウの制御#
チャット履歴は cache/history.json ファイルに保存され、ユーザーの次元で管理されます。この設計により、システムは複数のユーザーに同時にサービスを提供し、各ユーザーの独立したコンテキストを維持できます。_get_user_history と _update_user_history メソッドを通じて、プラグインは各ラウンドの対話に履歴情報を自動的に注入し、「記憶式」の問答体験を実現します。
履歴の長さにはウィンドウ制限(デフォルトで 10 ラウンド)を設けており、コンテキストの規模を制御し、モデルの処理負担を過度に増加させず、TOKEN の消費を避けています。キャッシュの更新は同期書き込み操作で行われ、システムのクラッシュや停電などの異常時に情報が失われないようにしています。
async def clear_user_history(self, user_id: str):
"""指定されたユーザーの履歴をクリア"""
user_id = str(user_id)
if user_id in self.history:
del self.history[user_id]
self._save_history()
reply = "チャット履歴をクリアしました"
else:
reply = "ユーザーのチャット履歴が見つかりません"
return reply
さらに、コマンド /clear chat_history を通じてユーザー履歴を積極的にクリアすることもサポートされており、プライバシーや再対話のための便利さを提供します。このメカニズムにより、プラグインは持続性を持ちながら、ユーザーに主導的な制御の空間を保持します。
8. DEBUG & LOG#
デバッグポイントを打ち、print マークを使用することは非常に良いテスト習慣です。私は WeChat 開発から一つのテクニックを学びました —— print ("FUCK")。長期間の実行中に時折クラッシュが発生することがありますが、その際にログに特定の文字を出力することで、問題を特定する際に文字列を直接検索して迅速に特定できます。FUCK は間違いなく面白い方法です。
この記事は Mix Space によって xLog に同期更新されました。元のリンクは https://fmcf.cc/posts/technology/ncatbot-multimodal-plugin