← 기사 목록
日本語https://zenn.dev/topics/llm/feed

🪡この春、マルチエージェントOSSをゼロから作ってApache-2.0で公開した話

추출된 키워드

49
マルチエージェント・オーケストレーター OSS·5Praxia·5Apache-2.0·4個人 → 組織へのメモリ自動昇格·45 層メモリスタック·43 経路評価エンジン·4v0.1.0·3fuzzy dedup·3RRF·3Reciprocal Rank Fusion·3LiteLLM·3Z-score 正規化·3LLM 自己評価·3Outcome correlation·3Frequency·3Sleep-time Consolidator·3L4 MarkdownStore·3LangChain·3CrewAI·3AutoGen·3PyPI·3L5 GraphLayer·3L3 SharedMemory·3L2 PromotionEngine·3L1 PersonalMemory·3GitHub·3LangMem·2docx·2python-pptx·2Anthropic·2Gemini·2Claude·2GPT-5·2pgvector·2TiDB Vector·2Graphiti·2Hindsight·2Zep·2Letta·2Mem0·2qwen-local·2gemma-local·2Ollama·2KMS-backed トークン暗号化·2OAuth·2RBAC·2SSO·2FastAPI·2Streamlit·2

원문

14,537
🪡この春、マルチエージェントOSSをゼロから作ってApache-2.0で公開した話

🪡この春、マルチエージェントOSSをゼロから作ってApache-2.0で公開した話

Zennfes Spring 2026「この春、始めたこと」エントリ記事です。

TL;DR

2026 年春、マルチエージェント・オーケストレーター OSS「Praxia」 を 1 ヶ月でゼロから作って公開しました(Apache-2.0)。

差別化のコアは「個人 → 組織へのメモリ自動昇格」。シニアエンジニアが磨いた「効くプロンプト」が個人の引き出しに留まる問題を、5 層メモリスタック + 3 経路評価エンジン で解いています。

何を「始めた」のか — 春の決断

2025 年末から、LangChain / CrewAI / AutoGen を業務で触ってきて、ある 構造的な違和感 がずっと残っていました。

エージェントの精度を本当に分ける要素は、ライブラリ選定でもモデル選定でもなく、

「ドメイン特化の試行錯誤の蓄積」ではないか?

そしてその蓄積は、ほぼ必ず特定のシニアの頭の中(Cursor / VSCode / Obsidian)に閉じ込められている。退職と同時に蒸発する暗黙知に、汎用フレームワークは無力 でした。

2026 年 4 月、本業の合間にコードを書き始めました。1 ヶ月後、

v0.1.0
を PyPI と GitHub に公開しました。

なぜ既存フレームではダメだったのか

実際に試した上での 4 つの壁:

何が起きていたか
設定の複雑さ 「とりあえず動く」までに 2-3 日。本番判断が下せない
暗黙知が届かない 効くプロンプトはシニアの個人空間に閉じている
評価のエビデンス不足 「動く」が「効く」を保証しない
エージェントが停滞する 一度作ったらフィードバックループが回らない

特に 2 番目が深刻でした。汎用エージェントフレームは「強い primitives」は提供しますが、「組織に知識を蓄積していく」プロセス自体は実装者の責任 に丸投げされています。

Praxia のコア設計 — 5 層 + 3 経路

ここからは技術選択の話。

5 層メモリスタック

L1 PersonalMemory   個人ごと(JSON / Mem0 / Letta / Zep / Hindsight / LangMem の 6 backend)
L2 PromotionEngine  夜間バッチ。L1 → L3 への昇格判定
L3 SharedMemory     組織全体。RBAC ゲート、時間減衰
L4 MarkdownStore    git 管理、PR レビュー必須、不変
L5 GraphLayer       任意(Zep / Graphiti)、関係抽出

肝は「L1 → L4 が手動ではない」こと。Sleep-time Consolidator(夜間バッチ)が個人メモリをスキャンし、組織知へ自動昇格させます。

3 経路の昇格エンジン

メモリ昇格を 3 つの独立シグナル で並行評価:

  • 頻度(Frequency): N 人以上で繰り返される事項
  • アウトカム相関(Outcome correlation): 受注 / PR 承認 / テスト合格などの成果と共起
  • LLM 自己評価(Self-eval): 0..1 で「組織知候補度」スコア

最終スコアは加重ブレンドし、いずれか 1 経路でも決定的なら昇格。単一機構依存(=どれか 1 つが落ちると全部落ちる)を意図的に避けています。

実装: praxia/memory/promoter.py(~250 LoC)。

「自分で書ける」を成立させた設計選択

1 ヶ月で書ききれたのは、4 つの設計選択のおかげでした。

① 7 拡張ポイント

すべての拡張点を同じ

praxia.extensions.Registry
プリミティブで実装:
拡張ポイントLoC 目安エントリポイント
Connector(Box / Notion / Slack 等)~50
praxia.connectors
Memory backend~80
praxia.memory_backends
File parser~30
praxia.parsers
Output exporter~30
praxia.exporters
OAuth provider~20
praxia.oauth_providers
Skill~50
praxia.skills
Flow~50
praxia.flows

コアファイルを編集せず、pyproject.toml の entry-point だけで拡張可能」を満たしているので、自分で書くときの認知負荷が低い。

② Apache-2.0 で全機能同梱(paywall なし)

SSO(Google / Microsoft Entra / Okta / GitHub / Keycloak)・RBAC・監査ログ・per-user OAuth(13 プロバイダ)・KMS-backed トークン暗号化(AWS / Azure / GCP / Vault / local)を すべて OSS コアに収録

商用エージェント基盤が "Enterprise tier" として paywall する機能の大半が、Apache-2.0 で同梱されています。これは導入時の判断スピードに直結します。

③ LiteLLM で 100+ プロバイダ

主要 LLM プロバイダの違い(Anthropic の

response_format
非対応、GPT-5.x の
temperature
不可、Azure のデプロイ名形式など)を LiteLLM レイヤで吸収。Praxia 本体には特定プロバイダの API キーが入らない設計。

完全オフライン運用も可能(Ollama +

gemma-local
/
qwen-local
+
backend=json
)。

④ Streamlit UI を「捨てやすい」設計に

UI は Streamlit で組みましたが、バックエンドは全部 praxia serve(FastAPI)としても起動可能。将来 Next.js / モバイルに差し替えるとき UI レイヤだけ捨てれば良い構造。

v0.1.0 で何を入れて、何を入れなかったか

入れたもの(意図的にフルセット):

  • 5 層メモリ + 3 経路昇格エンジン
  • 6 業務スキル(投資 / 営業 / 設計 / 購買 / 特許 / 法務)
  • 6 LTM backend + Composite/Routed 並列融合
  • 13 プロバイダの per-user OAuth(kintone 対応も含む)
  • SSO + RBAC + ACL + 監査ログ
  • 自律エージェント(LLM 駆動の tool-use ループ)
  • Document Designer(
    python-pptx
    /
    docx
    を sandbox 実行してデザイン済みファイル出力)
  • 8 言語の i18n(en / ja / zh-CN / ko / es / fr / de / pt-BR)

入れなかったもの(意図的に v0.2 以降):

  • マルチテナント GUI:OSS は「単一組織で self-host」想定。SaaS 級のテナント分離は Open Core の有償版で
  • PDF 出力:LibreOffice 経由のワークフロー推奨
  • Pinecone / Weaviate / Qdrant の native backend:Composite/Routed で
    mem0
    経由ラップ可能なので優先度低

何を作らないか」のリストは「何を作るか」のリストと同じくらい重要でした。

ローンチで起きたこと(1 週間の記録)

  • 5/12: 60 秒デモ動画を YouTube 公開 → README + ランディングに埋込
  • 5/13: Google Analytics 4 + Search Console 連携完了
  • 5/13: PyPI 公開 →
    pip install praxia
    で動作確認
  • 5/14: docs 整備(quickstart の「初回ログイン」セクション補強 — bootstrap admin の API キーの取り出し方を明文化)
  • 5/15: OG image 1200×630 を専用デザインに差替、Twitter / LinkedIn でリッチプレビュー化

完璧主義に陥らず「Done > Perfect」で出し続けるのが、個人開発で続けるコツだと改めて感じています。

ハマったポイント TOP 3

「動くものを 5 週間で出す」走りだったので、本当に時間を喰った 3 つはすべて 記憶層と昇格エンジン に関するものでした。

1. 3 経路スコアの「ばらつくスケールをどう混ぜるか」

PromotionEngine は L1 → L3 への昇格判定を Frequency / Outcome correlation / LLM self-eval の 3 シグナルで並列に評価します。これが想像より厄介でした:

  • Frequencyは 0..∞ の整数 (同一事実への参照数)
  • Outcome correlationは 0..1 の比率 (該当事実が共起したタスクの成功率)
  • LLM self-evalは 0..1 連続値、ただし非決定的で provider ごとにスコア分布が違う(GPT-5 は厳しめ中央値 0.4、Claude は緩めの 0.7、Gemini は二峰性)

最初の実装は単純な加重平均でしたが、frequency が無制限なので「片手間に L1 で何度も触っただけの事実」が全部上位に来る挙動になりました。最終的にこの形:

@dataclass(frozen=True)
class PromoteSignal:
    frequency: float       # raw count
    outcome_corr: float    # 0..1
    self_eval: float       # 0..1, median of N=3 LLM calls

def decide_promotion(sig: PromoteSignal, cfg: PromoteConfig) -> bool:
    # Z-score normalize on a rolling 30-day population, then sigmoid
    z_freq = sigmoid((sig.frequency - cfg.freq_mean) / cfg.freq_std)
    # OR logic with per-path threshold
    return (
        z_freq          > cfg.freq_threshold        # 0.85
        or sig.outcome_corr > cfg.outcome_threshold # 0.70
        or sig.self_eval    > cfg.self_eval_threshold # 0.80
    )

ポイント:

  • Z-score 正規化で frequency の暴走を抑制
  • OR ロジック + 経路ごと閾値にして「1 経路でも決定的なら昇格」 — 単一機構依存(=どれか 1 つに頼ると全部落ちる)を意図的に回避
  • LLM self-eval は N=3 の中央値で非決定性を平均化(N=5 までやってみたが効果がプラトー)
  • decide_promotion
    純粋関数なので、過去の昇格ログを再生してパラメータ感度分析ができる

「Z-score → sigmoid → OR with thresholds」 に辿り着くまで設計を 5 回くらいやり直しました。教訓: シグナル統合は加重平均から始めるな。経路ごとの decisive threshold + OR が結果として一番堅い。

2. Composite backend の「fan-out 検索 × 単一書込」を整合させる

Composite backend は複数 backend (JSON / Mem0 / TiDB / Letta…) に並列で検索クエリを送り、Reciprocal Rank Fusion (RRF) で融合します。一方、書込は

write_to=
で指定した 1 つに集約します。

この非対称性で 3 つの落とし穴にハマりました。

(a) あとから backend を追加すると過去データが見えない

Composite(backends=[A, B], write_to="A")
で運用していて、後から C を追加すると C には過去の書込が一切ない。検索 fan-out で「ヒットする backend が 2 つ、しない backend が 1 つ」という不均衡状態に。ドッグフーディング中に「ある backend だけ検索品質が低い」という形で発覚しました。

replay_writes(source=A, target=C, since=...)
という管理 API を後追いで追加。リバランス時に必要。

(b) write_to が落ちた時の graceful degradation

write_to が落ちた時、書込だけ止めて読みは続行、という選択肢が一見魅力的。が、これをやると 「検索でヒットしていた record が次回検索で消える」(write_to が復旧して書込が走らないため) という現象が起きうる。

最終的に graceful degradation を諦めて raise する 方針に。半端な可用性は eventual consistency の上にデータの真偽問題を重ねてしまう、と判断しました。「落ちている時は落ちていると正直に言う」のが結局一番運用しやすい。

(c) RRF 融合で同点 tie-breaking

複数 backend が 同じ record_id を rank 1 で返す とスコアが完全同点。tie-breaking ルールを定義しないと backend の登録順依存 で順序が変動し、CI でテストが flaky になる。

(rrf_score, timestamp DESC, backend_priority)
の lexicographic tie-breaking を導入。これで CI 環境でも順序再現性が出るように。

3. Sleep-time Consolidator の冪等性

夜間バッチで L1 → L3 昇格を再実行する設計上、同じ事実を 2 回昇格させない ことが冪等性の鍵です。

record_id
ベースの厳格 dedup だけでは不十分でした:
  • 同じユーザが「お客様 X は ROI を最優先」と「X さんは ROI 重視」と別文面で 2 回 L1 に記録 → 異なる record_id だが意味的に同一
  • LLM 自己評価は 非決定的: 同じテキストでも 0.78 / 0.82 / 0.76 のように揺れる → 閾値ぎりぎりで「昨日昇格しなかったが今日した」が起きる

そこで fuzzy dedup を導入:

def is_duplicate(candidate: Record, l3_recent: list[Record]) -> bool:
    # Already-promoted check: any L3 record within recent window whose
    # embedding cosine-sim >= threshold is treated as duplicate
    cand_vec = embed(candidate.text)
    return any(
        cosine_sim(cand_vec, r.embedding) >= 0.92
        for r in l3_recent  # already filtered to last 30 days
    )

難しかったのは 2 つの閾値(類似度 + ウィンドウ)のチューニング:

設定起きる問題
0.95 / 7 日 (厳しめ) 表現が違うだけの「同じ事実」が複数の L3 に並ぶ
0.85 / 90 日 (緩め) 新しいが類似した事実」(顧客 Y の同様傾向など)が抑制される
0.92 / 30 日 (採用値) 手動でラベル付けした 50 件のセットで false-merge / false-split が両方 < 5%

決定打は 両方向のメトリクスを同時に instrument した こと。「重複を見逃した率」だけでなく 「本来別事実なのに重複扱いした率」も常時計測 する。片側だけ追うと必ずどちらかに偏ったチューニングになります。これは PromotionEngine 全体の校正ループにも横展開しました。

数字で見る v0.1.0

項目
コードベース~25,000 LoC(Python + テスト + i18n)
テスト431 件 PASS
サポート LLM プロバイダ100+ via LiteLLM
同梱コネクタ19 種(Pull + Push 両対応)
Per-user OAuth プロバイダ13 種
ドキュメント英 + 日 で 30,000 字超
開発期間約 5 週間(夜と週末)

次の 3 ヶ月でやること

  • v0.2: TiDB Vector / pgvector 公式 backend(現状は
    mem0
    ラップ経由)
  • v0.2: localhost loopback OAuth(
    praxia serve
    不要にする)
  • v0.3: マルチテナント Org 機能(Open Core の入り口)
  • ドキュメント: 英語版チュートリアルの拡充
  • コミュニティ: Discord 開設、Discussions の活性化

同じ立場の人へ

個人 OSS を始めるかどうか悩んでいる方へ、5 週間走り抜けて感じたことを 3 つだけ。

  • 完成度より公開頻度:
    v0.1.0
    で出した方が、
    v0.0.4
    で磨き続けるより必ず学びが多い
  • 差別化を 1 行で:Praxia の場合は「個人 → 組織メモリ自動昇格」。これが言えないと記事も書けない、HN も書けない、PR も来ない
  • OSS の真の競合は『同じ問題を解いてる商用 SaaS』:LangChain でも CrewAI でもなく、有償エージェントプラットフォームの paywall。それを Apache-2.0 で同梱するだけで差別化になる

おわりに

⭐ Star / 🍴 Fork / Issue / PR をお待ちしています。
github.com/praxia-dev/praxia

もしこの記事が気に入ったら、デモ動画(60 秒)も見てみてください:

https://youtu.be/o_6NbjJU1AA

「個人の天才性 × 組織の継続性」を AI で結びつける — それが Praxia の春から始まったミッションです。