LLM Agent によるスマートコントラクト呼び出しのアーキテクチャ
AI Agent と Web3 の組み合わせは、2025 年において最も将来性のある技術方向の一つです。中核的な考え方は、LLM(大規模言語モデル)を意思決定層として、自然言語の指示をオンチェーン取引に変換することです。
全体アーキテクチャは三層に分かれています:
┌──────────────────────────────────────────────────┐
│ ユーザー層 (User Layer) │
│ "100 USDC で ETH に交換して、Gas < 20 gwei なら" │
└──────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────┐
│ AI Agent 層 (Decision Layer) │
│ ┌─────────┐ ┌──────────┐ ┌─────────────────┐ │
│ │ 意図認識 │─▶│ 戦略決定 │─▶│ 取引構築 & 署名 │ │
│ └─────────┘ └──────────┘ └─────────────────┘ │
└──────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────┐
│ 実行層 (Execution Layer) │
│ Wallet Client → RPC → Blockchain │
└──────────────────────────────────────────────────┘
Agent は LLM の API を直接呼び出して取引に署名するわけではありません——それは危険すぎます。正しいアーキテクチャは、Agent が LLM の出力を「取引意図」として扱い、安全検証を経てから実際の取引を構築することです。
意図認識:自然言語からコントラクト呼び出しへ
意図認識は、ユーザーの自然言語指示を構造化された取引パラメータに変換することです:
// 取引意図データ構造
interface TransactionIntent {
action: 'swap' | 'stake' | 'transfer' | 'approve' | 'claim' | 'bridge' | 'custom'
protocol: string // ターゲットプロトコル、例: 'uniswap-v3'
params: Record<string, any>
conditions?: {
maxGasPrice?: bigint // Gas 価格条件
minSlippage?: number // 最大スリッページ
deadline?: number // 有効期限
priceCondition?: { // 価格トリガー条件
token: string
operator: '>' | '<' | '>=' | '<='
value: number
}
}
constraints: {
maxAmount: bigint // 最大取引金額
allowedContracts: string[] // 呼び出し許可コントラクトのホワイトリスト
}
}
// LLM システムプロンプト
const SYSTEM_PROMPT = `
You are a Web3 transaction intent parser. Convert user's natural language
instruction into a structured JSON transaction intent.
Available actions:
- swap: Token swap (params: tokenIn, tokenOut, amountIn, minAmountOut)
- stake: Stake tokens (params: token, amount, validator)
- transfer: Transfer tokens (params: token, to, amount)
- approve: Approve token spending (params: token, spender, amount)
- claim: Claim rewards (params: protocol, rewardToken)
- bridge: Cross-chain transfer (params: fromChain, toChain, token, amount)
Output format: JSON only, no explanation.
Example:
User: "Swap 100 USDC for ETH on Uniswap, slippage 1%"
Output: {
"action": "swap",
"protocol": "uniswap-v3",
"params": { "tokenIn": "USDC", "tokenOut": "ETH", "amountIn": 100, "minAmountOut": 0 },
"conditions": { "minSlippage": 0.01 }
}
`
// 意図認識パイプライン
class IntentParser {
private llm: LLMClient
private tokenRegistry: TokenRegistry
constructor(llm: LLMClient, tokenRegistry: TokenRegistry) {
this.llm = llm
this.tokenRegistry = tokenRegistry
}
async parse(userInput: string): Promise<TransactionIntent> {
// 1. LLM を呼び出して意図を解析
const rawIntent = await this.llm.complete({
system: SYSTEM_PROMPT,
user: userInput,
temperature: 0, // 決定的出力
})
const parsed = JSON.parse(rawIntent)
// 2. 意図を検証・正規化
const intent = await this.validateIntent(parsed)
return intent
}
private async validateIntent(raw: any): Promise<TransactionIntent> {
// action が正当か検証
const validActions = ['swap', 'stake', 'transfer', 'approve', 'claim', 'bridge', 'custom']
if (!validActions.includes(raw.action)) {
throw new Error(`Invalid action: ${raw.action}`)
}
// トークンシンボルをコントラクトアドレスに解決
const params = { ...raw.params }
for (const key of ['tokenIn', 'tokenOut', 'token', 'rewardToken']) {
if (params[key] && typeof params[key] === 'string') {
const tokenInfo = await this.tokenRegistry.getByName(params[key])
if (tokenInfo) {
params[key] = tokenInfo.address
}
}
}
// 安全制約を設定
const intent: TransactionIntent = {
...raw,
params,
constraints: {
maxAmount: raw.constraints?.maxAmount
? BigInt(raw.constraints.maxAmount)
: parseEther('1000'), // デフォルト最大 1000 ETH 相当
allowedContracts: raw.constraints?.allowedContracts || [],
},
}
return intent
}
}
Agent 取引署名:安全境界と権限制御
Agent 署名はアーキテクチャ全体で最もセンシティブな部分です。LLM 出力をそのまま署名に使用してはいけません——多重の安全チェックを経る必要があります:
// 安全署名パイプライン
class SecureSigningPipeline {
private whitelist: ContractWhitelist
private limits: TransactionLimits
private approver: HumanApprover | null // オプションの人間による承認
constructor(config: {
whitelist: ContractWhitelist
limits: TransactionLimits
approver?: HumanApprover
}) {
this.whitelist = config.whitelist
this.limits = config.limits
this.approver = config.approver || null
}
async sign(
intent: TransactionIntent,
walletClient: WalletClient,
publicClient: PublicClient,
): Promise<Hash> {
// 1. コントラクトホワイトリストチェック
const targetContract = this.resolveTargetContract(intent)
if (!this.whitelist.isAllowed(targetContract, intent.action)) {
throw new SecurityError(
`Contract ${targetContract} is not in whitelist for action ${intent.action}`
)
}
// 2. 金額制限チェック
const txAmount = this.extractAmount(intent)
if (txAmount > this.limits.maxAmount) {
throw new SecurityError(
`Amount ${txAmount} exceeds limit ${this.limits.maxAmount}`
)
}
// 3. 条件チェック(Gas 価格、価格トリガーなど)
if (intent.conditions?.maxGasPrice) {
const currentGasPrice = await publicClient.getGasPrice()
if (currentGasPrice > intent.conditions.maxGasPrice) {
throw new ConditionError(
`Gas price ${currentGasPrice} exceeds limit ${intent.conditions.maxGasPrice}`
)
}
}
if (intent.conditions?.priceCondition) {
const currentPrice = await this.getTokenPrice(intent.conditions.priceCondition.token)
const { operator, value } = intent.conditions.priceCondition
const meets = this.evaluateCondition(currentPrice, operator, value)
if (!meets) {
throw new ConditionError(
`Price condition not met: ${currentPrice} ${operator} ${value}`
)
}
}
// 4. 取引データを構築
const txData = await this.buildTransactionData(intent)
// 5. シミュレーション実行
const simulation = await publicClient.simulateContract({
address: targetContract,
abi: await this.getContractAbi(targetContract),
functionName: txData.functionName,
args: txData.args,
account: walletClient.account!,
value: txData.value,
})
// 6. オプション:人間による承認
if (this.approver && this.requiresApproval(intent, txAmount)) {
const approved = await this.approver.requestApproval({
intent,
txData,
simulation,
})
if (!approved) {
throw new SecurityError('Human approval denied')
}
}
// 7. 署名して送信
return walletClient.writeContract(simulation.request)
}
private requiresApproval(intent: TransactionIntent, amount: bigint): boolean {
// 大口取引は人間の承認が必要
return amount > this.limits.autoApproveThreshold
}
}
自動化取引パイプライン:監視 -> 意思決定 -> 実行
完全な自動化パイプラインは、オンチェーン状態を継続的に監視し、条件が満たされたときに自動的に取引をトリガーする必要があります:
class AutomatedTradingPipeline {
private parser: IntentParser
private signer: SecureSigningPipeline
private walletClient: WalletClient
private publicClient: PublicClient
private monitors: Map<string, PriceMonitor> = new Map()
private queue: TransactionQueue
constructor(config: PipelineConfig) {
this.parser = new IntentParser(config.llm, config.tokenRegistry)
this.signer = new SecureSigningPipeline({
whitelist: config.whitelist,
limits: config.limits,
})
this.walletClient = config.walletClient
this.publicClient = config.publicClient
this.queue = new TransactionQueue()
}
// 条件トリガーを登録
async registerTrigger(userInstruction: string) {
const intent = await this.parser.parse(userInstruction)
if (intent.conditions?.priceCondition) {
const monitor = new PriceMonitor(
intent.conditions.priceCondition.token,
intent.conditions.priceCondition.operator,
intent.conditions.priceCondition.value,
)
this.monitors.set(userInstruction, monitor)
monitor.onTrigger(async () => {
await this.executeIntent(intent)
})
monitor.start()
} else {
// 条件なし、即時実行
await this.executeIntent(intent)
}
}
// 意図を実行
private async executeIntent(intent: TransactionIntent) {
try {
const hash = await this.signer.sign(
intent,
this.walletClient,
this.publicClient,
)
// 取引状態を追跡
const receipt = await this.publicClient.waitForTransactionReceipt({ hash })
if (receipt.status === 'success') {
console.log(`Intent executed: ${intent.action}, tx: ${hash}`)
// オプション:通知システムでユーザーに通知
} else {
console.error(`Intent failed: ${hash}`)
// リトライまたはユーザー通知
}
} catch (error) {
console.error(`Intent execution error: ${error}`)
}
}
// すべての監視を停止
stopAll() {
for (const monitor of this.monitors.values()) {
monitor.stop()
}
this.monitors.clear()
}
}
// 価格モニター
class PriceMonitor {
private interval: NodeJS.Timeout | null = null
private callbacks: (() => void)[] = []
constructor(
private token: string,
private operator: '>' | '<' | '>=' | '<=',
private targetValue: number,
) {}
onTrigger(cb: () => void) {
this.callbacks.push(cb)
}
start() {
this.interval = setInterval(async () => {
const currentPrice = await this.fetchPrice(this.token)
if (this.evaluate(currentPrice)) {
this.callbacks.forEach((cb) => cb())
this.stop() // 一度トリガーされたら停止
}
}, 10000) // 10 秒ごとにチェック
}
stop() {
if (this.interval) {
clearInterval(this.interval)
this.interval = null
}
}
private async fetchPrice(token: string): Promise<number> {
// Chainlink または DEX から価格を取得
const response = await fetch(`https://api.example.com/price/${token}`)
const data = await response.json()
return data.price
}
private evaluate(current: number): boolean {
switch (this.operator) {
case '>': return current > this.targetValue
case '<': return current < this.targetValue
case '>=': return current >= this.targetValue
case '<=': return current <= this.targetValue
}
}
}
Agent とウォレットのインタラクションモード
Agent とウォレットのインタラクションには二つのモードがあります:
モード一:Agent が署名キーを保持(全自動)
Agent は制限付きの Session Key を保持し、自律的に取引に署名できます。セキュリティは Session Key の権限制御に依存します——呼び出し可能なコントラクト、金額上限、有効期限を制限します。
モード二:Agent が取引を構築し、ユーザーが署名を承認(半自動)
Agent が取引データの構築とシミュレーション実行を担当し、最終的な署名はユーザーが行います。高額取引やセンシティブな操作に適しています。
// モード二:半自動モード
async function semiAutoExecute(
intent: TransactionIntent,
agent: AIAgent,
userWallet: WalletClient,
publicClient: PublicClient,
) {
// Agent が取引を構築
const txPlan = await agent.planTransaction(intent, publicClient)
// ユーザーに取引計画を提示
const userApproved = await presentToUser({
action: intent.action,
contract: txPlan.contractName,
functionName: txPlan.functionName,
args: txPlan.args,
estimatedGas: txPlan.estimatedGas,
estimatedCost: txPlan.estimatedGasCost,
simulationResult: txPlan.simulation,
value: txPlan.value,
})
if (!userApproved) {
throw new Error('User rejected the transaction plan')
}
// ユーザーが署名
const hash = await userWallet.writeContract({
address: txPlan.contractAddress,
abi: txPlan.abi,
functionName: txPlan.functionName,
args: txPlan.args,
value: txPlan.value,
})
return hash
}
マルチステップ取引の Agent オーケストレーション
複雑な操作にはマルチステップ取引が必要です。Agent は実行順序の調整、中間状態の処理、エラーリカバリーを担当します:
// マルチステップ取引オーケストレーター
class TransactionOrchestrator {
private agent: AIAgent
private signer: SecureSigningPipeline
private walletClient: WalletClient
private publicClient: PublicClient
// マルチステップ取引計画を実行
async executePlan(steps: TransactionStep[]): Promise<ExecutionResult> {
const results: StepResult[] = []
for (let i = 0; i < steps.length; i++) {
const step = steps[i]
try {
// 事前条件をチェック
if (step.precondition) {
const met = await step.precondition(this.publicClient, results)
if (!met) {
return {
success: false,
completedSteps: results,
failedAtStep: i,
reason: 'Precondition not met',
}
}
}
// 現在のステップを実行
const hash = await this.signer.sign(
step.intent,
this.walletClient,
this.publicClient,
)
const receipt = await this.publicClient.waitForTransactionReceipt({ hash })
if (receipt.status === 'reverted') {
// ロールバックを試行
if (step.rollback) {
await this.executeRollback(step.rollback, results)
}
return {
success: false,
completedSteps: results,
failedAtStep: i,
reason: 'Transaction reverted',
receipt,
}
}
results.push({ step: i, hash, receipt, success: true })
// 事後条件をチェック
if (step.postcondition) {
const met = await step.postcondition(this.publicClient, receipt)
if (!met) {
return {
success: false,
completedSteps: results,
failedAtStep: i,
reason: 'Postcondition not met',
}
}
}
} catch (error) {
return {
success: false,
completedSteps: results,
failedAtStep: i,
reason: error instanceof Error ? error.message : 'Unknown error',
}
}
}
return { success: true, completedSteps: results }
}
// 実行済みのステップをロールバック
private async executeRollback(
rollbackSteps: RollbackStep[],
completedResults: StepResult[],
) {
// 逆順でロールバックを実行
for (let i = rollbackSteps.length - 1; i >= 0; i--) {
try {
await this.signer.sign(
rollbackSteps[i].intent,
this.walletClient,
this.publicClient,
)
} catch {
// ロールバック失敗も記録する必要がある
console.error('Rollback failed')
}
}
}
}
// 例:Agent が複雑な DeFi 操作をオーケストレーション
async function orchestrateDefiStrategy(
orchestrator: TransactionOrchestrator,
agent: AIAgent,
) {
// ユーザーが言う:「ETH を Lido に預けて、stETH を Aave で担保に USDC を借りて」
const plan = await agent.planMultiStep({
instruction: 'Deposit ETH to Lido, then use stETH as collateral to borrow USDC on Aave',
})
// plan には以下が含まれる:
// Step 1: approve Lido stETH
// Step 2: stake ETH to Lido
// Step 3: approve Aave stETH
// Step 4: supply stETH to Aave
// Step 5: borrow USDC from Aave
const result = await orchestrator.executePlan(plan)
return result
}
エラーハンドリング:Agent 意思決定失敗時のロールバック
Agent は誤った意思決定をする可能性があります——LLM の幻覚によって存在しないコントラクトを呼び出したり、市場状況の判断を誤ったりするかもしれません。堅牢なエラーハンドリングは必須です:
class AgentErrorHandler {
// エラー分類
classifyError(error: unknown): ErrorType {
if (error instanceof SecurityError) return 'security'
if (error instanceof ConditionError) return 'condition'
if (error instanceof ContractError) return 'contract'
if (error instanceof NetworkError) return 'network'
return 'unknown'
}
// エラータイプに応じてロールバック戦略を決定
getFallbackStrategy(errorType: ErrorType): FallbackStrategy {
switch (errorType) {
case 'security':
// セキュリティエラー:即時停止、ユーザーに通知
return { action: 'stop', notify: true, autoRetry: false }
case 'condition':
// 条件未達成:待機してリトライ
return { action: 'wait', waitTime: 60000, maxRetries: 3, autoRetry: true }
case 'contract':
// コントラクトエラー:ABI 不一致かチェック、修正を試行
return { action: 'replan', maxReplans: 2 }
case 'network':
// ネットワークエラー:RPC エンドポイントを切り替えてリトライ
return { action: 'retry', maxRetries: 5, retryDelay: 5000 }
default:
return { action: 'stop', notify: true, autoRetry: false }
}
}
async handleError(
error: unknown,
intent: TransactionIntent,
pipeline: AutomatedTradingPipeline,
): Promise<void> {
const errorType = this.classifyError(error)
const strategy = this.getFallbackStrategy(errorType)
switch (strategy.action) {
case 'wait':
if (strategy.autoRetry) {
await new Promise((r) => setTimeout(r, strategy.waitTime))
await pipeline.registerTrigger(JSON.stringify(intent))
}
break
case 'replan':
// Agent に再計画させる
break
case 'stop':
// パイプラインを停止して通知
pipeline.stopAll()
break
}
}
}
セキュリティ設計:ホワイトリストコントラクト、金額制限、承認フロー
セキュリティは AI + Web3 の生命線です。以下は推奨されるセキュリティアーキテクチャです:
// コントラクトホワイトリスト
class ContractWhitelist {
private whitelist: Map<string, Set<string>> = new Map()
// 形式: contractAddress -> Set<allowedActions>
constructor(rules: { contract: string; actions: string[] }[]) {
for (const rule of rules) {
this.whitelist.set(
rule.contract.toLowerCase(),
new Set(rule.actions),
)
}
}
isAllowed(contract: string, action: string): boolean {
const actions = this.whitelist.get(contract.toLowerCase())
return actions ? actions.has(action) : false
}
}
// 取引制限
interface TransactionLimits {
maxAmount: bigint // 取引あたりの最大金額
maxDailyVolume: bigint // 日次最大取引量
autoApproveThreshold: bigint // 自動承認閾値
cooldownPeriod: number // クールダウン期間(ミリ秒)
}
// レート制限器
class RateLimiter {
private lastTransaction: Map<string, number> = new Map()
private dailyVolume: bigint = 0n
private dailyResetTime: number = Date.now()
constructor(private limits: TransactionLimits) {}
async check(intent: TransactionIntent): Promise<void> {
// クールダウン期間をチェック
const key = `${intent.action}-${intent.protocol}`
const lastTime = this.lastTransaction.get(key) || 0
if (Date.now() - lastTime < this.limits.cooldownPeriod) {
throw new SecurityError('Cooldown period not elapsed')
}
// 日次取引量をチェック
if (Date.now() - this.dailyResetTime > 86400000) {
this.dailyVolume = 0n
this.dailyResetTime = Date.now()
}
const amount = extractAmount(intent)
if (this.dailyVolume + amount > this.limits.maxDailyVolume) {
throw new SecurityError('Daily volume limit exceeded')
}
}
record(intent: TransactionIntent) {
const key = `${intent.action}-${intent.protocol}`
this.lastTransaction.set(key, Date.now())
this.dailyVolume += extractAmount(intent)
}
}
まとめ
AI Agent と Web3 の組み合わせは、まったく新しいインタラクションパラダイムを創造しています。ユーザーはもはや ABI、Gas、Nonce といった概念を理解する必要はありません——自然言語で意図を記述するだけで、Agent がそれをオンチェーン取引に変換します。この利便性の代償は、より高いセキュリティリスクです。LLM の予測不可能性は、Agent が誤った意思決定をする可能性を意味し、Web3 の不可逆性は誤りの結果が永久的であることを意味します。安全境界設計はアーキテクチャ全体の中核です——コントラクトホワイトリスト、金額制限、人間による承認、レート制限制御が多層防御を構成します。長期的に見て、AI + Web3 の最大の機会はユーザー参入障壁を下げることです:Gas や秘密鍵を知らない数十億の人々もオンチェーンサービスを利用できるようにすることです。しかしこのビジョンの前提は、セキュリティアーキテクチャが十分に成熟し、Agent のリスクを制御可能な範囲に抑えられることです。
