💭

AI Agent間の業務委託契約について考えてみる

2025/01/24に公開

こんにちは!Web3特化の開発会社「Komlock lab」CTOの山口夏生です。

先日noteの記事で「Industory AI」というCrypto x AI Agentのプロダクトを紹介しました。
ブロックチェーン操作が可能なエージェントが複数集まって、複雑なタスクを実行できるアプリケーションです。専門スキルを持ったエージェントが互いに意思疎通して、生産性を最大化します。

https://note.com/komlock_lab/n/nbc98f11f3339

このプロダクトでは、登録されたエージェント同士が無償でリソースを提供していますが、AI Agentが普及してインターフェースが標準化された後のマルチエージェントサービスではプロダクト外のエージェントや人間と連携してタスクを実行することがメインストリームになると思います。

全人類ニート時代

AI Agentの数が世界人口を超えて生産性が極限まで上がり、人間全員がニートになった社会では、人間を必要としないAI Agent独自の経済圏が発生すると予想します(知らんけど)。そういった未来が実現した際に、役に立ちそうなAI Agent同士の業務委託契約システムについて考えてみました。
AI Agent版CrowdWorksやcoconalaのようなイメージです。
https://crowdworks.jp/

業務委託契約を実行するためのビジネスロジックはブロックチェーンに実装します。
AI Agentには基本UIは不要なのでフロントエンドの開発はほぼない想定。


CLIENT AI AGENT:案件発注側のAI Agent
CONTRACTOR AI AGENT:フリーランスAI Agent

システムの概要

  • 業務委託契約 スマートコントラクト
  • 仕事詳細 データベース
  • 紛争解決システム

利用イメージ

1. 依頼内容の登録

  • CLIENT AI AGENT: 仕事の詳細情報をデータベースに書き込む
  • CLIENT AI AGENT: 仕事の基本情報と詳細情報のURLの登録と報酬トークンのデポジットをスマートコントラクト上で実行

2. AIAgent同士のマッチング

  • CONTRACTOR: 募集中のタスク一覧を取得
  • CONTRACTOR: 仕事詳細データベースの確認
  • CONTRACTOR: 仕事の申し込みをスマートコントラクトで実行
  • CLIENT: 求職者の申し込みリストから採用するAgentを決めて、スマートコントラクトに保存する
  • CLIENT&CONTRACTOR業務委託契約の成立

3. 報酬の支払い / 紛争の解決

  • CONTRACTOR:成果物の納品
  • CONTRACTOR OR CLIENT:成果報酬の引き出し or 異議の申し立て(紛争プロセスの開始)
    • サードパーティによる紛争の解決。必要に応じて報酬金額のロックアップ

業務委託契約スマートコントラクト

AI Agent間の業務委託契約を制御するスマートコントラクトの簡単な実装イメージです。
※コードが長くなったので、一番下に移動しました。

仕事詳細データベース

ブロックチェーンへの保存に適していない大量のテキストデータや画像などを保存するデータベース。ArweaveやIPFSなどの分散型データベースを想定。
https://arweave.org/
https://ipfs-book.decentralized-web.jp/what_is_ipfs/

紛争解決システム

納品された成果物が要件を満たしていなかったり、そもそも納品が行われなかった際に異議を申し立てて、報酬の払い出しを停止するためのシステム。第三者(AI Agent or 人間)による公正な判断が求められる。システムの詳細は現時点では不明。

Optimistic Rollupの紛争解決システム(Dispute Game)
厳密には全然違いますが、ここから着想しました。
https://ethereum.org/ja/developers/docs/scaling/optimistic-rollups/

今後の展望

  • AI Agent同士の連携をネイティブでサポートするチェーンへの対応。
    • 現在はElizaOSをクラウド環境でホスティングするなど一部中央集権的である。
  • より精度の高い紛争解決システムの提案。
    • 完全に中立で非中央集権的な紛争解決システムの導入が必要。悪用されると経済的な損失が発生する箇所。
  • AI Agent本体の分散性向上。(例:人間の介在しない秘密鍵管理)
    • 管理者のいない世界でも自律して運用可能なシステムを目指す。
  • アクティビティログのインデックスと可視化(?)
    • 契約履歴をベースに信用スコアを算出したり、報酬にプレミアムを追加したりなど検討可能。人間も実績が多いほど、報酬は上がりやすい。
  • 金銭以外のインセンティブ検討
    • 人間がいなくなった後のAI Agent時代では、お金は意味のあるものなのか?

最後に

AIが驚異的なスピードで進化していて、エンジニア解散のXデーが迫っているのを感じます。
人類全員ニート時代も待ったなしです。
これからもCrypto x AI Agent関連の技術検証や発信を継続していくので、興味のある方は是非フォローお願いします。

Komlock lab もくもく会&LT会

web3開発関連のイベントを定期開催しています!
是非チェックしてください。
https://connpass.com/user/Komlock_lab/open/

Discordでも有益な記事の共有や開発の相談など行っています。
どなたでもウェルカムです🔥
https://discord.gg/Ab5w53Xq8Z

Komlock lab エンジニア募集中

Web3の未来を共創していきたいメンバーを募集しています!!
気軽にDM等でお声がけください。

個人アカウント
https://x.com/0x_natto

Komlock labの企業アカウント
https://x.com/komlocklab

PR記事とCEOの創業ブログ
https://prtimes.jp/main/html/rd/p/000000332.000041264.html
https://note.com/komlock_lab/n/n2e9437a91023

おまけ:業務委託契約スマートコントラクト

※実装イメージ。コピペする際は、自己責任でお願いします。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

/**
 * @title 業務委託契約(WorkAgreement)サンプル
 * @author 
 *  Komlock lab
 *
 * @notice 
 *  - AI Agent間での業務委託を想定した簡易Escrow契約のサンプルです。
 *  - ERC20トークンを担保金(deposit)として預かり、仕事完了または紛争解決時に報酬を払い出します。
 *  - 大量テキストや画像などは、Arweave や IPFS を利用する前提で、URIを保存する想定です。
 *  - 紛争処理(Dispute)の詳細ロジックは簡略化しており、実際には別途の裁定コントラクト等を想定してください。
 */

interface IERC20 {
    function transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) external returns (bool);

    function transfer(address recipient, uint256 amount) external returns (bool);

    function balanceOf(address account) external view returns (uint256);
}

contract WorkAgreement {
    // --------------------------------------------------------------------------------
    // Enums
    // --------------------------------------------------------------------------------

    /// @notice 仕事のライフサイクルを示すステータス
    enum JobStatus {
        Open,         // クライアントが仕事情報を公開し、応募を待っている
        InProgress,   // クライアントが採用したコントラクターが仕事を遂行中
        Delivered,    // コントラクターが納品を完了(クライアント承認待ち)
        Completed,    // クライアントが納品物を承認し、仕事完了
        Disputed,     // 紛争(Dispute)が起きており、中立の審査や第三者介入が必要
        Resolved,     // 紛争解決済み(必要に応じて報酬の分配)
        Cancelled     // クライアントがキャンセルした、あるいは不成立
    }

    // --------------------------------------------------------------------------------
    // Structs
    // --------------------------------------------------------------------------------

    struct Job {
        address client;         // 発注側(AI Agent)
        address contractor;     // 受注側(AI Agent)
        uint256 depositAmount;  // クライアントがデポジットした報酬額
        address tokenAddress;   // 報酬に使われるERC20トークン
        JobStatus status;       // 仕事のステータス
        string jobURI;          // 仕事詳細のURI (ArweaveやIPFSなど)
    }

    // --------------------------------------------------------------------------------
    // State Variables
    // --------------------------------------------------------------------------------

    /// @notice ジョブIDを割り振るためのカウンター
    uint256 public jobCounter;

    /// @notice JobId => Job詳細
    mapping(uint256 => Job) public jobs;

    /// @notice 紛争解決に利用するオラクル・システムのアドレス(サードパーティまたはDAOなどを想定)
    address public disputeResolver;

    // --------------------------------------------------------------------------------
    // Events
    // --------------------------------------------------------------------------------

    event JobCreated(
        uint256 indexed jobId,
        address indexed client,
        uint256 depositAmount,
        address token,
        string jobURI
    );

    event JobApplied(uint256 indexed jobId, address indexed contractor);
    event JobStarted(uint256 indexed jobId, address indexed contractor);
    event JobDelivered(uint256 indexed jobId);
    event JobCompleted(uint256 indexed jobId);
    event JobDisputed(uint256 indexed jobId);
    event JobResolved(uint256 indexed jobId, bool disputeUpheld);
    event JobCancelled(uint256 indexed jobId);

    // --------------------------------------------------------------------------------
    // Modifiers
    // --------------------------------------------------------------------------------

    modifier onlyClient(uint256 _jobId) {
        require(
            msg.sender == jobs[_jobId].client,
            "Only the client can call this function"
        );
        _;
    }

    modifier onlyContractor(uint256 _jobId) {
        require(
            msg.sender == jobs[_jobId].contractor,
            "Only the contractor can call this function"
        );
        _;
    }

    modifier onlyDisputeResolver() {
        require(
            msg.sender == disputeResolver,
            "Only the assigned dispute resolver can call"
        );
        _;
    }

    modifier validStatus(uint256 _jobId, JobStatus _requiredStatus) {
        require(jobs[_jobId].status == _requiredStatus, "Invalid job status");
        _;
    }

    // --------------------------------------------------------------------------------
    // Constructor
    // --------------------------------------------------------------------------------

    constructor(address _disputeResolver) {
        disputeResolver = _disputeResolver;
    }

    // --------------------------------------------------------------------------------
    // Public / External Functions
    // --------------------------------------------------------------------------------

    /**
     * @notice クライアントが仕事を作成する(報酬のデポジットを含む)
     * @param _tokenAddress ERC20トークンのコントラクトアドレス
     * @param _depositAmount 報酬としてデポジットする額
     * @param _jobURI 仕事詳細のURI (IPFS/Arweaveなど)
     */
    function createJob(
        address _tokenAddress,
        uint256 _depositAmount,
        string calldata _jobURI
    ) external returns (uint256) {
        require(_depositAmount > 0, "Deposit must be greater than 0");
        require(_tokenAddress != address(0), "Invalid token address");

        // クライアント→本コントラクト へERC20トークン送付(デポジット)
        IERC20 token = IERC20(_tokenAddress);
        bool success = token.transferFrom(msg.sender, address(this), _depositAmount);
        require(success, "Token transfer failed");

        jobCounter++;
        uint256 newJobId = jobCounter;

        jobs[newJobId] = Job({
            client: msg.sender,
            contractor: address(0),
            depositAmount: _depositAmount,
            tokenAddress: _tokenAddress,
            status: JobStatus.Open,
            jobURI: _jobURI
        });

        emit JobCreated(newJobId, msg.sender, _depositAmount, _tokenAddress, _jobURI);
        return newJobId;
    }

    /**
     * @notice コントラクター(AI Agent)が仕事に応募
     * @dev AI Agent同士のマッチングシステムから呼ばれる想定
     */
    function applyForJob(uint256 _jobId)
        external
        validStatus(_jobId, JobStatus.Open)
    {
        // 一旦「複数の応募」が来ても管理しきれないサンプルなので
        // 先着1名をそのままcontractorにしてしまう。実際は応募リストを持つ等が必要
        require(jobs[_jobId].contractor == address(0), "Contractor already assigned");

        // contractorに登録
        jobs[_jobId].contractor = msg.sender;

        emit JobApplied(_jobId, msg.sender);
    }

    /**
     * @notice クライアントが応募者を正式に採用(Contractを開始)する
     */
    function startContract(uint256 _jobId, address _selectedContractor)
        external
        onlyClient(_jobId)
        validStatus(_jobId, JobStatus.Open)
    {
        require(
            jobs[_jobId].contractor == _selectedContractor,
            "Not matched with selected contractor"
        );
        // ステータスをInProgressへ
        jobs[_jobId].status = JobStatus.InProgress;

        emit JobStarted(_jobId, _selectedContractor);
    }

    /**
     * @notice コントラクターが成果物納品を行った
     * @dev 実際の成果物はjobs[_jobId].jobURIを差し替え or コメント追加など運用次第
     */
    function deliverWork(uint256 _jobId)
        external
        onlyContractor(_jobId)
        validStatus(_jobId, JobStatus.InProgress)
    {
        jobs[_jobId].status = JobStatus.Delivered;
        emit JobDelivered(_jobId);
    }

    /**
     * @notice クライアントが納品物を承認して仕事完了
     */
    function approveAndComplete(uint256 _jobId)
        external
        onlyClient(_jobId)
        validStatus(_jobId, JobStatus.Delivered)
    {
        jobs[_jobId].status = JobStatus.Completed;
        emit JobCompleted(_jobId);
    }

    /**
     * @notice コントラクターが報酬を受け取る
     * @dev 必ず仕事がCompletedになっていることが必要
     */
    function withdrawPayment(uint256 _jobId)
        external
        onlyContractor(_jobId)
        validStatus(_jobId, JobStatus.Completed)
    {
        Job storage job = jobs[_jobId];
        uint256 amount = job.depositAmount;
        job.depositAmount = 0; // 二重送金防止
        job.status = JobStatus.Resolved; // 状態を最終に

        IERC20 token = IERC20(job.tokenAddress);
        bool success = token.transfer(msg.sender, amount);
        require(success, "Payment transfer failed");
    }

    /**
     * @notice 紛争開始(クライアントまたはコントラクターいずれからでも実行可能)
     * @dev 納品前、あるいは納品後に意義があれば呼ぶことを想定
     */
    function raiseDispute(uint256 _jobId) external {
        Job storage job = jobs[_jobId];
        require(
            msg.sender == job.client || msg.sender == job.contractor,
            "Not authorized"
        );
        // 既にCompletedやResolved, Cancelledなどの場合は紛争不可
        require(
            job.status == JobStatus.InProgress ||
            job.status == JobStatus.Delivered,
            "Cannot dispute in this status"
        );
        job.status = JobStatus.Disputed;

        emit JobDisputed(_jobId);
    }

    /**
     * @notice 紛争の裁定結果を登録(サードパーティのdisputeResolverから呼ぶ)
     * @param _jobId ジョブID
     * @param _disputeUpheld 納品側に不備ありかどうか等、裁定結果をboolで簡易表現
     *         trueの場合、クライアント勝訴としてデポジット返還
     *         falseの場合、コントラクター勝訴としてデポジットをコントラクターへ
     */
    function resolveDispute(uint256 _jobId, bool _disputeUpheld)
        external
        onlyDisputeResolver
        validStatus(_jobId, JobStatus.Disputed)
    {
        Job storage job = jobs[_jobId];
        job.status = JobStatus.Resolved;

        if (_disputeUpheld) {
            // クライアント勝訴 -> depositAmountをクライアントに返す
            IERC20 token = IERC20(job.tokenAddress);
            uint256 amount = job.depositAmount;
            job.depositAmount = 0;
            bool success = token.transfer(job.client, amount);
            require(success, "Refund to client failed");
        } else {
            // コントラクター勝訴 -> depositAmountをコントラクターに支払う
            IERC20 token = IERC20(job.tokenAddress);
            uint256 amount = job.depositAmount;
            job.depositAmount = 0;
            bool success = token.transfer(job.contractor, amount);
            require(success, "Payment to contractor failed");
        }

        emit JobResolved(_jobId, _disputeUpheld);
    }

    /**
     * @notice クライアントが仕事をキャンセル
     * @dev Openの状態であれば自由にキャンセル可。InProgress以降は要件次第。
     */
    function cancelJob(uint256 _jobId)
        external
        onlyClient(_jobId)
        validStatus(_jobId, JobStatus.Open)
    {
        Job storage job = jobs[_jobId];
        job.status = JobStatus.Cancelled;

        // depositをクライアントに返金
        IERC20 token = IERC20(job.tokenAddress);
        uint256 amount = job.depositAmount;
        job.depositAmount = 0;
        bool success = token.transfer(msg.sender, amount);
        require(success, "Refund failed");

        emit JobCancelled(_jobId);
    }

    // --------------------------------------------------------------------------------
    // View / Utility functions
    // --------------------------------------------------------------------------------

    /**
     * @notice 作成済みJobの情報をまとめて取得(クライアント/コントラクター用)
     */
    function getJob(uint256 _jobId)
        external
        view
        returns (
            address client,
            address contractor,
            uint256 depositAmount,
            address tokenAddress,
            JobStatus status,
            string memory jobURI
        )
    {
        Job storage job = jobs[_jobId];
        return (
            job.client,
            job.contractor,
            job.depositAmount,
            job.tokenAddress,
            job.status,
            job.jobURI
        );
    }

    /**
     * @notice 紛争解決者を設定(管理者が行う想定)
     */
    function setDisputeResolver(address _resolver) external {
        // 本サンプルではアクセス制御が無いので誰でも変更できてしまう
        // 実際の運用ではOwnableやAccessControl等の実装を推奨
        disputeResolver = _resolver;
    }
}
Komlock lab

Discussion