ラベル AI の投稿を表示しています。 すべての投稿を表示
ラベル AI の投稿を表示しています。 すべての投稿を表示

2026年8月31日月曜日

Github copilot のトークン消費を抑えるコツ

Github copilot のトークン消費を抑えるコツ

お金ないねん

結論

  • 動作確認は可能な限り自分で行う
  • ループさせない

これが結論です
以下詳細に記載します

環境

  • macOS 26.6.2
  • VSCode 1.135.0
  • @github/copilot: 1.0.81-0

CI などで行う動作確認コマンドは自分で実行する

例えば Python プロジェクトであれば CI などで以下の処理をすると思います

  • フォーマット (isort/black)
  • リント (flake8)
  • 型チェック (pyright)
  • テスト (pytest)
  • セキュリティチェック (bandit,semgrep,trivy など)
  • その他 (npm 関連の処理など)

当然ローカルでも行うのでこれらの動作確認用の最低限のコマンドは自分で行うようにしましょう
copilot さんは実装後に大抵テストを自分で回して問題ないことを確認してくれるのですがプロジェクトの構成がわかっておらず例えば

  • pytest test

などと実行することがあります
でも実はプロジェクトは uv 管理化なのでテストは

  • uv run pytest test

で実行しないと必ずエラーになるという場合があります
SKILL.md や AGETNTS.md を書いてテストの方法などを先にインプットしておけば間違えること少なくなるのですがそれでも変な実行をすることがあります

要するに copilot がテストコマンドを間違えるとテスト -> 失敗 -> テスト -> 失敗 -> テスト… と繰り返し行うのでそこで無駄なトークンの消費が発生するためあっという間にクレジットも消費してしまいます

なのでコーディングしてもらったら自分で動作確認し「動作確認はこちらで完了しています」と伝えると copilot も作業をそこでやめるのでトークンの節約につながります

自分で動作確認した結果エラーになった場合はそのエラー結果をそのまま伝えれば OK です

面倒ですがこれだけでもかなりクレジットの節約になる印象です

もしコマンドまで実行してもらう場合は正確なコマンドを渡したほうがいい

「テストの場合はこのコマンドを使ってください」という感じで最初に明示的にコマンドを渡すとコマンドをミスしたりしないので安心です

基本的にコマンドを実行する場合は確認のダイアログが出るのでそのタイミングで正しいコマンドを渡すでも OK です

CLI ツールは使わない

Copilot CLI などはトークンの消費を抑えたいのであれば極力使うのはやめましょう

CLI エージェント系は基本的に「ループ」を前提に作られており「長時間の長いタスク」を得意としています

「こういう結果になるまでこの確認コマンドを使って確認して成功するまで実装を続けてね」

というような処理で使います
当然ですが処理が長くなるとコンテキストが増大しトークンの消費も莫大になります

CLI エージェントの設定や LLM 側 (もしくは Proxy 側) でトークン数の Max は決められますがそれでも CLI エージェント系は大量のトークンを必要とする印象です

なので基本的には無料枠だけを使いたい場合には CLI エージェント系は使わないほうがいいかなと思います
月末ですぐにリセットされる状態でトークンを使い切りたい場合にはいいかもしれません

ループさせるようなタスクは命令しない

前述のように copilot に何度もリトライさせるようなタスクはやらせないようにしましょう

動作確認をこちらで行ったりスクリプトを実行する場合は可能な限りこちらで行いその結果だけを教えてあげると良いです

シンプルなタスクを渡す

単純に考えて大きなタスクは大量のトークンを消費すると思っていいかなと思っています

複雑なタスクは一発で成功する可能性が低く成功しても成果物が微妙なケースも有るため結局繰り返すことになります
また作ってもらったものを理解するために再度命令したりもするためその分無駄なトークンを使うことになります

また大きなタスクにはほぼループ処理が必須となり前述の通りループは大量にトークンを消費します

小さいタスクを確実にこなしてもらい作ってもらうたびに自力で理解することで節約する感じです

これだけ聞くと AI をフルに活用できていないストレスの溜まりそうな作戦ですが今回はあくまでも消費を抑える Tips なのでご了承ください

無駄なレビューとかさせない

現状のコードを確認してもらうためのレビューやコード全体を解読し内容を説明してもらうようなレビューや極力やめましょう

それでもコードやプロジェクトの内容を説明してほしい場合はモジュールやファイルを限定して説明、レビューしてもらうようにしましょう

曖昧な質問/適当な質問をしない

基本的にはやってほしいことをしっかりまとめてから依頼したほうがいいです

TODO の作成や thinking があるので適当に質問してもやることをまとめてくれるのが最近の LLM の良いところではあるのですがその分トークンは消費するので節約したい場合は TODO の作成などは自分でやりましょう

もし使い切った場合はどうするか

自分は以下のどれかです

  • 別のサービスを使う (チャッピー codex など
  • ローカル LLM に頼る
  • 素直に有料プランに申し込む

最後に

本当は雑に渡してループで完了するまでやってほしいところですが無料枠だとまず無理です

わざわざクレジットの消費を意識して AI コーディングするのがストレスという人は素直に Pro プランなどに申し込みましょう

もしくはハイスペック GPU マシンを購入してローカル LLM を使いましょう

AI に依存しまくると AI がないとほぼコーディングできなくなるので AI (お金) とうまく付き合っていく必要があります

一番良いのはローカル LLM かなと思いますがそれなりのハイスペックマシンがないとお話にならないので難しい限りです

2026年7月3日金曜日

Issue を作成したらあとは AI 先生に修正してもらえばいいじゃない

Issue を作成したらあとは AI 先生に修正してもらえばいいじゃない

Codex Skills 編

環境

  • Ubuntu 24.04
  • codex 0.142.5
  • Gitlab 18.11.6

仕組み

目的: GitLab の Issue を自動検知し、AI(Codex)を使ってコード修正〜MR作成までを自動化するエージェント。


構成要素

ファイル 役割
main.py エントリポイント。初期化してスケジューラを起動
scheduler.py 定期ポーリングループ。Issue の処理を制御
mygitlab.py GitLab API から agent ラベルの Issue を取得
state.py SQLite で処理済み Issue を管理(二重処理防止)
codex.py Codex CLI をサブプロセスで起動し AI エージェントを実行
codex_skills/ Codex に渡すスキル定義(修正・テスト・コミット・MR作成)
config.py 全設定値の一元管理

動作の流れ(概要)

  1. Issue 検知: POLL_INTERVAL(60秒)おきに GitLab をポーリングし、agent ラベルの付いた Issue を取得
  2. 重複防止: 取得した Issue が SQLite に記録済みかチェックし、処理済みならスキップ
  3. AI 修正: 未処理の Issue に対して Codex CLI を起動。gitlab-issue-agent スキルに Issue の情報(ID・タイトル・説明)を渡す
  4. Codex の自律作業: Codex がスキルの指示に従い、コード修正 → テスト実行 → コミット → MR 作成を自律的に実施
  5. 完了記録: 処理が終わった Issue ID を SQLite に保存し、次のサイクルへ

ポイント

  • 冪等性: SQLite による処理済み管理で同じ Issue を何度も処理しない
  • 疎結合: Codex とのやり取りは環境変数とプロンプトのみで、スキルの追加・変更が容易
  • 自律性: MR 作成まで人手不要。レビューアー・アサインも自動設定済み

流れ

基本は対象の Issue 分ループして次々修正し MR を作成します

サンプルコード

ツリー

.
├── codex_skills
│   ├── create-mr
│   │   ├── scripts
│   │   │   └── create_mr.py
│   │   └── SKILL.md
│   ├── git-commit
│   │   └── SKILL.md
│   ├── gitlab-issue-agent
│   │   └── SKILL.md
│   └── run-tests
│       └── SKILL.md
├── codex.py
├── config.py
├── logger.py
├── main.py
├── mygitlab.py
├── README.md
├── scheduler.py
├── state.db
├── state.py
└── systemd
    └── codex-agent.service

main.py

import logging

from logger import setup_logger
from scheduler import run
from state import init_db

logger = logging.getLogger(__name__)

if __name__ == "__main__":
    setup_logger()
    init_db()
    logger.info("Starting gitlab bug fix agent")
    try:
        run()
    except Exception:
        logger.exception("Agent stopped due to an unhandled exception")
        raise

scheduler.py

import logging
import time

from codex import run_codex
from config import POLL_INTERVAL
from mygitlab import get_issues
from state import is_processed, mark_processed

logger = logging.getLogger(__name__)


def run():
    logger.info("Scheduler started")
    while True:
        logger.info("Polling GitLab issues")
        issues = get_issues()
        logger.info("Fetched %d issues", len(issues))

        for issue in issues:
            iid = issue["iid"]

            if is_processed(iid):
                logger.info("Skipping already processed issue %s", iid)
                continue

            logger.info("Processing issue %s: %s", iid, issue.get("title", ""))
            run_codex(issue)

            mark_processed(iid)
            logger.info("Marked issue %s as processed", iid)

        logger.info("Polling complete; sleeping for %s seconds", POLL_INTERVAL)
        time.sleep(POLL_INTERVAL)

codex.py

import logging
import os
import subprocess

from config import (
    CODEX_PATH,
    GITLAB_TOKEN,
    GITLAB_URL,
    MR_ASSIGNEE_ID,
    MR_REVIEWER_IDS,
    PROFILE,
    PROJECT_ID,
    REPO_PATH,
    SANDBOX,
    SONNET_API_KEY,
)

logger = logging.getLogger(__name__)


def run_codex(issue):
    env = os.environ.copy()
    issue_id = issue["iid"]

    # 環境変数設定(codex_skills/create-mr/scripts/create_mr.py で使用)
    env["ISSUE_ID"] = str(issue_id)  # MR のブランチ名生成に使用
    env["ISSUE_TITLE"] = issue["title"]  # MR のタイトル生成に使用
    env["GITLAB_URL"] = GITLAB_URL  # GitLab API エンドポイント
    env["GITLAB_TOKEN"] = GITLAB_TOKEN  # GitLab API 認証
    env["PROJECT_ID"] = str(PROJECT_ID)  # MR 作成対象プロジェクト
    env["MR_ASSIGNEE_ID"] = str(MR_ASSIGNEE_ID)  # MR アサイン対象ユーザー
    env["MR_REVIEWER_IDS"] = ",".join(map(str, MR_REVIEWER_IDS))  # MR レビュアー
    env["SONNET_API_KEY"] = SONNET_API_KEY  # Codex CLI が使用する LLM API キー

    logger.info("Launching Codex for issue %s", issue_id)

    prompt = f"""
Use the skill gitlab-issue-agent.

Issue ID: {issue_id}
Title: {issue['title']}
Description: {issue['description']}
"""

    result = subprocess.run(
        [
            CODEX_PATH,
            "exec",
            "--cd",
            REPO_PATH,
            "--profile",
            PROFILE,
            "--sandbox",
            SANDBOX,
            prompt,
        ],
        env=env,
        check=True,
        capture_output=True,
        text=True,
    )

    if result.stdout:
        logger.info("Codex stdout:\n%s", result.stdout)
    if result.stderr:
        logger.warning("Codex stderr:\n%s", result.stderr)

    logger.info("Codex finished for issue %s", issue_id)

config.py

GITLAB_URL = "https://your-gitlab-url"
GITLAB_TOKEN = "glpat-xxx"
PROJECT_ID = 12345
TARGET_LABEL = "agent"

REPO_PATH = "/home/user/target"
CODEX_PATH = "/home/user/.local/bin/codex"
PROFILE = "sonnet"
SONNET_API_KEY = "dummy"
SANDBOX = "danger-full-access"

POLL_INTERVAL = 60

MR_ASSIGNEE_ID = 1
MR_REVIEWER_IDS = [2]

DB_PATH = "./path/to/state.db"
LOG_PATH = "./path/to/agent.log"

state.py

import logging
import sqlite3

from config import DB_PATH

logger = logging.getLogger(__name__)


def init_db():
    logger.info("Initializing state database at %s", DB_PATH)
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("""
    CREATE TABLE IF NOT EXISTS processed_issues (
        issue_id INTEGER PRIMARY KEY
    )
    """)
    conn.commit()
    conn.close()
    logger.info("State database is ready")


def is_processed(issue_id: int) -> bool:
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT 1 FROM processed_issues WHERE issue_id=?", (issue_id,))
    result = c.fetchone()
    conn.close()
    return result is not None


def mark_processed(issue_id: int):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("INSERT INTO processed_issues(issue_id) VALUES(?)", (issue_id,))
    conn.commit()
    conn.close()
    logger.info("Persisted processed issue %s", issue_id)

logger.py

import logging

from config import LOG_PATH


def setup_logger():
    logging.basicConfig(
        filename=LOG_PATH,
        level=logging.INFO,
        format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    )

mygitlab.py

import logging

import requests
from config import GITLAB_TOKEN, GITLAB_URL, PROJECT_ID, TARGET_LABEL

HEADERS = {"PRIVATE-TOKEN": GITLAB_TOKEN}

logger = logging.getLogger(__name__)


def get_issues():
    url = f"{GITLAB_URL}/api/v4/projects/{PROJECT_ID}/issues"
    params = {"labels": TARGET_LABEL, "state": "opened"}
    logger.info(
        "Fetching issues from GitLab project %s with label %s", PROJECT_ID, TARGET_LABEL
    )
    response = requests.get(url, headers=HEADERS, params=params)
    issues = response.json()
    logger.info("GitLab issues fetch completed with status %s", response.status_code)
    return issues

codex_skills/create-mr/SKILL.md

---
name: create-mr
description: Create a GitLab merge request.
---

Run the script:

python .codex/skills/create-mr/scripts/create_mr.py

codex_skills/create-mr/scripts/create_mr.py

import logging
import os
import subprocess
import sys
from pathlib import Path

import requests

SCRIPT_ROOT = Path(__file__).resolve().parents[3]
if str(SCRIPT_ROOT) not in sys.path:
    sys.path.append(str(SCRIPT_ROOT))

from logger import setup_logger

setup_logger()
logger = logging.getLogger(__name__)

issue_id = os.environ.get("ISSUE_ID")

branch = f"agent/issue-{issue_id}"

GITLAB_URL = os.environ["GITLAB_URL"]
TOKEN = os.environ["GITLAB_TOKEN"]
PROJECT_ID = os.environ["PROJECT_ID"]
MR_ASSIGNEE_ID = int(os.environ["MR_ASSIGNEE_ID"])
MR_REVIEWER_IDS = [int(x) for x in os.environ["MR_REVIEWER_IDS"].split(",")]

url = f"{GITLAB_URL}/api/v4/projects/{PROJECT_ID}/merge_requests"

headers = {"PRIVATE-TOKEN": TOKEN}

issue_title = os.environ.get("ISSUE_TITLE", f"Issue {issue_id}")

data = {
    "source_branch": branch,
    "target_branch": "main",
    "title": f"修正: {issue_title}",
    "assigned_to_id": MR_ASSIGNEE_ID,
    "reviewer_ids": MR_REVIEWER_IDS,
}

logger.info("Creating merge request for issue %s on branch %s", issue_id, branch)
response = requests.post(url, headers=headers, json=data)
logger.info("Merge request creation completed with status %s", response.status_code)

codex_skills/git-commit/SKILL.md

---
name: git-commit
description: Commit and push changes to a new branch.
---

Steps:

1. Create branch:
   agent/issue-<ISSUE_ID>

2. Commit:
   git add .
   git commit -m "fix: issue <ISSUE_ID>"

3. Push:
   git push -u origin <branch>

codex_skills/gitlab-issue-agent/SKILL.md

---
name: gitlab-issue-agent
description: End-to-end agent that processes a GitLab issue, applies fixes, runs tests, and creates a merge request.
---

You are an autonomous software engineering agent.

You will be given:

- Issue ID
- Title
- Description

# Your job

## Step 1: Understand the issue

- Identify the root cause
- Determine what needs to be fixed

## Step 2: Create a plan

Break down into tasks:

- files to modify
- logic changes
- tests needed

## Step 3: Implement fix

- Modify the codebase
- Keep changes minimal and clean

## Step 4: Run tests

Use the skill:

- run-tests

If tests fail:

- fix the issue
- retry until success (max 3 times)

## Step 5: Commit changes

Use skill:

- git-commit

## Step 6: Create MR

Use skill:

- create-mr

# Rules

- Do not break existing functionality
- Follow project conventions
- Prefer small commits

codex_skills/run-tests/SKILL.md

---
name: run-tests
description: Run Go validation and test suite.
---

Execute:

- go fmt ./...
- go vet ./...
- go build ./...
- go test ./...

If any step fails:

- Analyze the error
- Fix the issue
- Re-run

使い方

  • codex のインストールと各種設定
  • codex_skills にある各種 skill を .codex/skills 配下にコピー
  • config.pyの編集
  • codex_skills/run-tests/SKILL.md の編集
  • un run python main.py
  • Issue を作成し適切なラベルを振る

課題

  • Issue にしっかりと TODO ややるべきこと課題を記載する必要がある
    • Issue の内容が適当すぎると MR の修正内容ももうまく行かない
    • Issue のタスクが大きすぎるとうまくいかない
    • 可能な限り細かく TODO を書きつつ粒度を細かくする必要がある
  • main ブランチの pull タイミングをどうするか
    • 次の Issue を捌く前に一旦 main に戻るかそのブランチから続きを開発したいか
    • 前に作成した MR がマージされるまで次の Issue を捌くのを待つか
  • サンドボックス環境なのでネットワークや書き込み権限をしっかり与える必要がある
    • Codex でネットワーク操作 (今回だと push と MR 作成) をする場合にはほぼほぼ danger-full-access が必須そう
    • デフォルトは workspace-write なので書き込めるだけ、コードを修正したりなど (参考)
    • ちなみに Ubuntu だと bubblewrap を使ってサンドボックス化するのでインストールと sysctl の設定が必要
    • sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
    • セキュリティ的なことを考えるとインタラクティブモードが一番良さそう
    • codex を実行する環境自体をサンドボックス(コンテナや特別なVM)にする
  • 現状は 1Issue - 1MR になっている
    • AI が捌けるタスクの粒度上 Issue は可能な限り小さなタスクにする必要がある
    • ただ実際の現場だと MR に複数の Issue 対応を追加したいケースがある
    • MR が大量になってしまうのでレビューが大変になる (これも AI にお任せでいいかもしれないが)
  • スケジューラ側から codex 側に値を渡すのに環境を使っているができれば別の仕組みを考えたい
  • 処理した Issue かどうかの判断を sqlite で行っているが sqlite を使わずに別のラベルに付け替えるだけでも良さそう
    • sqlite にしておけば履歴的なこともできるメリットはある

所感

  • インタラクティブモードでコマンドの許可や権限をいちいち与えるのも面倒ではあるがインタラクティブモードのほうがセキュアなのかもしれない
  • 小さいタスクならこれで十分だが大きなタスクはまだ厳しいそう
  • VSCode + Copilot でやる AI 開発と Codex + インタラクティブモードでやる AI 開発と Codex の自動化とどれがいいかではなく結局は使い分けな気がする
  • Skills は必須ではないと思うがあったほうが作業は正確になると思う
    • すべて Codex CLI 入力時のプロンプトでまかなえる可能性もある
    • Skills にするとその辺りのやりたいことがコード化されるので見える化できるのと作業の安定性が図れそう
  • Codex CLI 以外の CLI AI エージェントでも試してみたい

最後に

Codex CLI の世界だけですべてカバーできないだろうか
結局 Codex CLI はプロンプトに対するアウトプットを返してくれるだけなのでスケジューラなど他の仕組みは自分で作るしか無いのだろうか

Codex のデスクトップアプリは「定期実行して」というとやってくれるらしいがそれと同じでなくてもいいが CLI にもほしいところ

2026年5月28日木曜日

ぼくの中での emacs は死んでしまったのかもしれない

ぼくの中での emacs は死んでしまったのかもしれない

さらば emacs …

結論

  • emacs は完全に AI 関連で遅れを取っている
  • Copilot や OpenAI の拡張はあるが「そうじゃない」感がすごい
  • VSCode + copilot の使い勝手に勝てない

以下詳細に綴ります

emacs 拡張を使えば各種 AI と連携はできるができるだけで公式レベルには勝てないと判断した

emacs にも copilot.el などの拡張があるので emacs から各種 AI と連携することはできます

ただ連携できるだけでエージェントや MCP 連携 (最近は MCP もほぼ使わない) がほぼ無力な印象です
ただチャットに質問しているだけで自動で編集してくれたり作成してくれたりはしません

これを執筆しているときはほぼ emacs は使っていないので何ともですが自分が copilot.el を試した時期では少なくともエージェントのような機能はありませんでした
今であれば拡張も充実してエージェント機能も充実しているかもしれませんがそれでも VSCode + copilot のエージェント機能には及ばない気がします

公式が出しているエディタに付属している公式が出しているエージェントが公式の LLM を使うという当たり前の正解

基本 AI コーディングには LLM は必須です
OpenAI や Claude、Gemini などバックエンドに専用の LLM が必要です
もちろんローカルでも OK です

すべての LLM に言えることとしてすべての LLM は挙動が違うと思っています
OpenAI だとこう答えるが Gemini だとこう答えるといういわゆる「クセ」みたいなのがあると思っています

LLM に対して質問するのは基本的にエージェントです
そのエージェントが LLM の挙動を知っているか知っていないかは AI コーディングにおいて非常に重要な点かなと思っています

例えば VScode + copilot でローカル LLM を呼び出すとしましょう
するとどうでしょうか普通に使っている copilot とは全く挙動が違って全然思い通りのコーディングをしてくれなかったなんて経験はないでしょうか
自分はかつて試したことがあり全く期待通りじゃなかったことを実感しています
もちろんローカル LLM の選択やマシンのスペックが低かったなど考えられる要因は様々あります

つまりエージェントには適した LLM があり間違った組み合わせのエージェントと LLM では AI コーディングの精度に大きな差が生まれるのです

ここまで説明すればだいたいわかると思いますが同じ公式が作ったエディタ+エージェント+LLMの組み合わせのほうが確実に良い AI コーディングができるに決まっているのです

それなのに自力でエージェントを作ってサードパーティなりローカルの LLM を呼び出し AI コーディングしてもらうのは (決して間違ってはいませんが) DRY や KISS の法則的には反しているような気が自分はしています

与えられたものを素直に使うのが良いということです

独自の AI エージェントを作る場合は別

上記の件に関して独自の AI エージェントを作る場合は別かなと思います

エージェントは自作する必要がありますし LLM もどこか選定し API をコールする必要があります
そのエージェントに何をさせるかはわかりませんが自分だけの自分専用のエージェントであれば作りましょう

だたその場合も CLI で動作するエージェントはたくさんあるのでもし自分にあった既製品があるのであればそれを使うほうが精度としては良くなることがあるかもしれません (もちろん自作のほうが良い挙動になる可能性もあります)

エージェントが賢くなりすぎて最近は MCP すらいらない気がする

かつてはエディタやエージェントから MCP にアクセスする必要がありプロセスを立ち上げていました
MCP は Ubuntu などの Linux 環境では容易に立ち上げられるし管理も用意だったのでその点では emacs との親和性はありました

しかし最近のエージェントではデフォルトでブラウザが扱えたりローカルに対してはコマンドを叩けたり外部リソースにも簡単にアクセスしたりとかつて MCP が必要だった機能をエージェントが補完してくれています (内部的に MCP をエージェントがコールしてくれているケースもありますが)

要するにエージェントが賢くなりすぎていろいろできるようになってしまったがゆえに MCP も独自で立てる必要がなくなった気がしています

もちろん MCP が不要とはいいませんが昔ほど AI コーディングにおいて MCP が必須なコンポーネントであるとは現在では思えません

RAG や他の AI プロトコルについては今回は触れませんが RAG に関しても自分は同等の意見です
また Agents.md などのコンテキストというかシステムプロンプト的なものも最近では書く必要を感じなくなりました

そうじゃない感について

自分は主に copilot を使うので比較対象が copilot になってしまうのですが copilot には「Copilot CLI」という CLI 環境で動作するエージェントもあります
それを使って emacs の拡張で足りない部分を補いことはできるかもしれませんが個人的にはそれが「そうじゃない感」を生み出している点です

開発するときには基本的にはエディタや IDE と向き合います
かつて自分が emacs で開発しているときは開発 VM に ssh しそこで tmux を立ち上げその tmux バッファないで emacs を -nw で emacs を立ち上げて開発していました

確かにその環境は素晴らしかったです
ssh して tmux バッファを開けばすでに emacs でコーディングもできるし他のバッファでアプリのビルドやテストができる状態でした
その環境自体は自分は今も好きです

ただ AI コーディングをするとなるとエディタや IDE だけで完結する必要があります (必要があるというかその方がはるかにストレスレスだと思っています)
AI に質問するのにわざわざバッファを切り替えたり AI に質問したあとでわざわざ手動でコピペするというのは今の時代だとかなりナンセンスな方法です
一昔前であればブラウザでチャッピー (ChatGPT) に質問しその結果をエディタ側にコピペしてという開発手法でしたがコンテキストの与え方を考えたりトークンの数を減らすために RAG を構築したりとかなり大変でした (それは今もやってるかもしれませんが)

AI コーディングするにあたってあっちこっち切り替えてコピペしては簡単な作業とは言えかなりストレスが溜まります
それが VSCode + copilot ではすべてその場で解決します
質問すればすべて回答しその回答結果を即時に勝手にコードに反映してくれます
もしかすると一部コピペする必要があるかもしれません
もしかすると一部のコマンドの許可を与えるためにボタンを押す必要があるかもしれません
それでもアプリを切り替えてあっちこっちいったりする必要はなく VSCode ないだけでほぼ確実に完結します

これが個人的に感じている AI コーディングにおける「そうじゃない感」です

勝っている点があるとすれば

正直ないような気もします
本当に一部のケースで(慣れの問題もありますが) emacs のほうが素早く作業できる場合はあります

  • インストール -> どちらも homebrew できる
  • CLI 環境 -> emacs は -nw、VSCode は code-server or SSH トンネルなど
  • 拡張 -> どちらもあり (elisp or JavaScript)
  • コード実行 -> どちらもあり
  • オープンソース -> どちらも
  • キーバインド -> どちらもできる

などなど emacs にしかできないことはほぼないような気がします (もちろん探せば全然ありますが表面上の利用用途としてはほぼなさそう)

それでも一部の作業はやはり emacs を使う

自分が今でも emacs を使っているケースがあります
それは単純なメモを取るときです

VSCode + copilot は何でもかんでも AI でやろうとするのでただメモを取りたいだけなのにコードサジェスチョンをしようとします
タイプ中に関係ないことをサジェスチョンされるのは嫌なのでプレーンテキストメモするときは emacs を使っています

また VSCode は基本的に 1 プロジェクトのみを開くことを対象にしているので現在開発しているプロジェクト以外を編集/閲覧したい場合には emacs を使います
一応 VSCode にも workspace という機能があり 1 つのウィンドウで複数のプロジェクトを開くことができるのですが AI のノイズになったり単純に邪魔になったりするので自分はあまり使っていません

emacs は今後も生き続けるはず

それでも emacs は今後も生き続けるはずです
emacs 自体の発展や拡張の発展が AI コーディングにより期待できるからです

先ほども記載しましたが tmux + emacs の環境は今でも好きだし使っています

またキーバインドによるショートカットや矩形選択などの編集などはまだ emacs のほうが速く作業できるケースもあるのでケースバイケースかなとも思っています
ただメインのアプリケーション開発においてはもう VSCode しか使っていません

もちろん emacs キーバインドは大好きで VSCode でも他のエディタや IDE、ターミナルソフトでも emacs キーバインドを使っています

最後に

AI エージェントが搭載されていないアプリはエディタや IDE だけではなく今後なくなってくるのかもしれません
また AI の肝はエージェントの実装と言っても過言ではないので優れたエージェントを持つアプリが生き残っていくのかもしれません

今回はその例がたまたま emacs vs VSCode だったというわけです

その他メモ

  • トラブル時にサーバに SSH ログインして作業するときも vim があれば十分なケースが多い
  • Microsoft 製のソフトウェアで唯一使っているソフトかもしれない

2026年2月4日水曜日

軽量ローカル LLM でコーディングするのは不可能ではないがやめたほうがいい

軽量ローカル LLM でコーディングするのは不可能ではないがやめたほうがいい

不可能ではないがフラストレーションがえぐいだけ
誰でも動かせる LLM じゃあ商売上がったりなんでそりゃ無理だよねって話

結論

  • copilot レベルの精度を出すには軽量 LLM ではほぼ不可能
    • 学習および機能不足すぎる
    • 軽量 LLM では tools/thinking がない
    • 単純に学習量が少ないのか回答が的はずれななケースが多くフラストレーションが高い
  • ただしマシンスペックが潤沢 (RTX 4090/5090、メモリ VRAM+RAM が128GB 以上) なら話は別
    • 大規模 LLM が動かせれば copilot と同等の UX が得られるかも
    • 「かも」なのは実際に試せていないため

試したエディタ/エージェント環境

  • VSCode 1.108.2
  • copilot chat プラグイン (標準装備)

使ってみた LLM

  • phi4-mini:3.8b (agent)
  • llama3.1:8b (agent)
  • llama3.2:3b (ask)
  • codegemma:7b (ask)
  • gemma3:4b (ask)
  • qwen3:8b (ask)

以下それぞれ使ってみた感想です
細かい精度は違いますが大枠は変わらなかったのでモデルごとではなくまとめて記載します

感想/所感

  • thinking がないから命令を丁寧にしなければならない
    • どのコンテキストに対して何をどうやって修正すればいいのか
    • 命令の意図を解釈するフェーズが LLM にないので可能な限り丁寧に命令しなければならない
    • copilot は雑な質問でも命令を解釈しコンテキストを読み込み意図を解釈してくれる
  • Agent が使えないケースが多い
    • 使えても使い物にならない
  • 結論としては大規模 LLM が必要
    • ローカル LLM を使って copilot 相当のことをしたいのであれば大規模 LLM が必要
    • 最低でも 20b ほどだが現実ラインでは 120b レベルが必要 (gpt-oss:120b など)
    • 実際それを動かすとなると推論に必要なグラボと何よりも VRAM + RAM が必要になる
    • 120b なら合わせて 128GB ほどメモリ領域が必要になる
    • deepseek-v3.1:671b などは現実レベルでローカルで動かすのはほぼ不可能ということになる
  • 使い方が悪かった可能性もある
    • 命令やコンテキストの与え方が悪い
    • 命令が抽象的にすぎる (が本当は抽象的に質問してもやってほしい)
    • コンテキストが多すぎて LLM が解釈できない
    • 逆にコンテキストが少なすぎて無視してしまう
  • トンチンカンな回答例
    • コンテキストで与えたコードを無視して回答する
    • 単純なサンプルコードだけ提示して終了
  • 適切な LLM の設定
    • Continue などはどのフェーズでどの LLM を使用するか設定できる
    • copilot にも auto があるがローカル LLM だと使えない (はず
    • Ask/Edit/Plan で使用する LLM をカスタムエージェントで設定できるのだろうか (おそらくできない

最後に

大規模 LLM が動作するマシンが30万だとすると現在の Copilot Pro は 1500 円くらいなので

  • 300000/1500=200

で200ヶ月分は使えるので素直に Copilot Pro に課金するのがいいかもしれません
ローカルで大規模 LLM は無料と言えば無料ですが本当に無料ではありません (電気代とか)
他にゲームやマイニングとかで用途があればあってもいいかもですが

2026年1月14日水曜日

AI エージェントを作ってみた感想

AI エージェントを作ってみた感想

これまでにBotはいくつか作っていました

https://blog.kakakikikeke.com/2025/10/no-life-no-bot.html

それもエージェントと言えばエージェントなのかもしれませんがもう少しエンジニアっぽい AI エージェントを作ってみたので感想やらつらつら残しておきます

作ったもの

  • 不正プロセス監視エージェント
  • 不正ログ監視エージェント
  • 株価予測エージェント

不正プロセス監視エージェントのソースコード

#!/usr/bin/env python3
import json
import os
import subprocess
import time
from collections import deque

from google import genai
from slack_sdk import WebClient

GEMINI_API_KEY = "xxx"
SLACK_TOKEN = "xoxb-xxx"
SLACK_CHANNEL = "#general"
CUSTOM_BASE_URL = "https://your-llm-domain/endpoint"

# バッチ処理の設定
BATCH_MODE = (
    os.getenv("BATCH_MODE", "true").lower() == "true"
)  # デフォルトはバッチモード
BATCH_SIZE = int(os.getenv("BATCH_SIZE", "5"))  # 一度に判定するプロセス数
BATCH_TIMEOUT = int(os.getenv("BATCH_TIMEOUT", "10"))  # タイムアウト時間(秒)
MONITOR_INTERVAL = int(os.getenv("MONITOR_INTERVAL", "30"))  # プロセス取得間隔(秒)

print(f"Mode: {'BATCH' if BATCH_MODE else 'SINGLE'}")
print(
    f"BATCH_SIZE: {BATCH_SIZE}, BATCH_TIMEOUT: {BATCH_TIMEOUT}s, MONITOR_INTERVAL: {MONITOR_INTERVAL}s"
)

client = genai.Client(
    api_key=GEMINI_API_KEY,
    http_options=genai.types.HttpOptions(base_url=CUSTOM_BASE_URL),
)

slack = WebClient(token=SLACK_TOKEN)

# 既に通知済みのプロセスを追跡(重複通知を避けるため)
notified_processes = set()


# --- プロセス情報取得 ---
def get_processes():
    """
    ps コマンドでシステム上の全プロセスを取得する。
    戻り値: [{"pid": "...", "user": "...", "cpu": "...", "mem": "...", "cmd": "..."}, ...]
    """
    try:
        # aux フラグで詳細情報を取得
        cmd = ["ps", "aux"]
        result = subprocess.run(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            check=True,
        )

        processes = []
        lines = result.stdout.strip().split("\n")

        # ヘッダースキップ
        for line in lines[1:]:
            parts = line.split()
            if len(parts) >= 11:
                processes.append(
                    {
                        "user": parts[0],
                        "pid": parts[1],
                        "cpu": parts[2],
                        "mem": parts[3],
                        "vsz": parts[4],
                        "rss": parts[5],
                        "cmd": " ".join(parts[10:]),
                    }
                )

        return processes

    except Exception as e:
        print(f"Error getting processes: {e}")
        return []


# --- LLM にプロセスの異常判定を依頼 ---
def analyze_process_single(process_info: dict) -> bool:
    """
    単一のプロセスを LLM で判定する。
    怪しいなら True を返す。
    """
    prompt = f"""
あなたは Linux サーバーのセキュリティ監視 AI です。
以下のプロセス情報が不審かどうか、以下の形式で答えてください。

プロセス情報:
- PID: {process_info['pid']}
- ユーザー: {process_info['user']}
- CPU使用率: {process_info['cpu']}%
- メモリ使用率: {process_info['mem']}%
- VSZ: {process_info['vsz']}
- RSS: {process_info['rss']}
- コマンド: {process_info['cmd']}

判断:
- 不審なら "SUSPICIOUS"
- そうでなければ "OK"

通常のシステムプロセス(kernel threads、systemd関連など)は OK と判定してください。
考察や理由は出力しないでください。
"""

    response = client.models.generate_content(
        model="gemini-2.5-pro",
        contents=prompt,
    )

    if response.text is None:
        print("No response text from LLM")
        return False

    result_text = response.text.strip().upper()
    return "SUSPICIOUS" in result_text


def analyze_processes_batch(processes: list) -> dict:
    """
    複数のプロセスを一度に LLM で判定する。
    戻り値: {pid: bool} の辞書
    """
    if not processes:
        return {}

    # プロセス情報をまとめてプロンプト作成
    processes_text = "\n".join(
        [
            f"{i+1}. PID: {p['pid']}, User: {p['user']}, CPU: {p['cpu']}%, Mem: {p['mem']}%, Cmd: {p['cmd']}"
            for i, p in enumerate(processes)
        ]
    )

    prompt = f"""
あなたは Linux サーバーのセキュリティ監視 AI です。
以下のプロセス情報が不審かどうか、それぞれを判定してください。

プロセス一覧:
{processes_text}

判定結果を以下の JSON フォーマットで出力してください:
{{
  "1": "OK",
  "2": "SUSPICIOUS",
  "3": "OK",
  ...
}}

JSONのキーはプロセス番号、値は "SUSPICIOUS" または "OK" です。
通常のシステムプロセス(kernel threads、systemd関連など)は OK と判定してください。
"""

    response = client.models.generate_content(
        model="gemini-2.5-pro",
        contents=prompt,
    )

    if response.text is None:
        print("No response text from LLM")
        return {p["pid"]: False for p in processes}

    # JSON を抽出して解析
    result_dict = {}
    try:
        # レスポンスから JSON を抽出
        json_str = response.text
        # JSONブロックの開始と終了を探す
        start_idx = json_str.find("{")
        end_idx = json_str.rfind("}") + 1
        if start_idx >= 0 and end_idx > start_idx:
            json_str = json_str[start_idx:end_idx]
            parsed = json.loads(json_str)

            # プロセスごとに判定結果をマッピング
            for idx, process in enumerate(processes, 1):
                result = parsed.get(str(idx), "OK").upper()
                result_dict[process["pid"]] = "SUSPICIOUS" in result
        else:
            # JSON が見つからない場合は全て False
            result_dict = {p["pid"]: False for p in processes}
    except json.JSONDecodeError as e:
        print(f"JSON parse error: {e}")
        result_dict = {p["pid"]: False for p in processes}

    return result_dict


# --- Slack 通知 ---
def notify(process_info: dict):
    message = f"""
:rotating_light: *Suspicious process detected!*
PID: {process_info['pid']}
User: {process_info['user']}
CPU: {process_info['cpu']}% | Mem: {process_info['mem']}%
Command: {process_info['cmd']}
"""
    slack.chat_postMessage(channel=SLACK_CHANNEL, text=message)


# --- フィルタリング関数 ---
def should_check_process(process_info: dict) -> bool:
    """
    監視対象外のプロセスをフィルタリング
    """
    cmd = process_info["cmd"]
    user = process_info["user"]

    # self プロセスは除外
    if "process_monitor_agent" in cmd:
        return False

    # root 関連の明らかに安全なプロセスは除外
    safe_keywords = [
        "kernel",
        "[",
        "]",
        "systemd",
        "sshd",
        "agetty",
        "bash",
        "sh",
        "grep",
        "ps",
        "vim",
        "nano",
        "sudo",
        "root",
        "ls -l",
        "sd-pam",
        "unbound",
        "cron",
        "sleep",
        "less",
        "/sbin/init",
        "canonical-livepatch",
        "snapd",
        "ubuntu-advantage/timer.py",
        "multipathd",
        "journalctl",
        "bpftrace",
        # 開発ツール関連
        "tmux",
        "mysqld",
        "redis-server",
        "code-server",
        "mosquitto",
        "ollama",
        "nginx",
        # postfix 関連
        "pickup -l -t unix -u -c",
        "tlsmgr -l -t unix -u -c",
        "qmgr -l -t unix -u",
        "/usr/lib/postfix/sbin/master",
        # docker 関連
        "dockerd",
        "docker-proxy",
        # apt 関連
        "apt-get",
        "packagekitd",
        "/usr/lib/update-notifier/apt-check",
        "tar -df alternatives.tar.0 -C /var/lib/dpkg alternatives",
        # EDR 関連
        "cbram",
        "cybereason-sensor",
        "cybereason-activeconsole",
    ]
    if any(keyword in cmd for keyword in safe_keywords):
        return False

    return True


# --- プロセス監視メイン処理 ---
def monitor_processes():
    """
    定期的にプロセスを取得して監視
    """
    print("Process monitor started.")

    if BATCH_MODE:
        # バッチモード
        monitor_processes_batch()
    else:
        # 単発モード
        monitor_processes_single()


def monitor_processes_single():
    """単発モード: 各プロセスを即座に判定"""
    while True:
        try:
            processes = get_processes()

            # フィルタリング
            filtered_processes = [p for p in processes if should_check_process(p)]

            for process in filtered_processes:
                try:
                    suspicious = analyze_process_single(process)
                    if suspicious:
                        pid = process["pid"]
                        if pid not in notified_processes:
                            print(f"[!] Suspicious process: {process}")
                            notify(process)
                            notified_processes.add(pid)
                    else:
                        print(f"[OK] {process['cmd']}")

                except Exception as e:
                    print(f"Error analyzing process: {e}")

            time.sleep(MONITOR_INTERVAL)

        except Exception as e:
            print(f"Error in monitor loop: {e}")
            time.sleep(MONITOR_INTERVAL)


def monitor_processes_batch():
    """バッチモード: プロセスをバッチでまとめて判定"""
    process_buffer = deque()
    last_batch_time = time.time()

    while True:
        try:
            current_time = time.time()
            processes = get_processes()

            # フィルタリング
            filtered_processes = [p for p in processes if should_check_process(p)]

            # バッファに追加
            for process in filtered_processes:
                process_buffer.append(process)

            # バッチ処理の条件: BATCH_SIZE に達したか、BATCH_TIMEOUT を超えたか
            should_process = len(process_buffer) >= BATCH_SIZE or (
                len(process_buffer) > 0
                and current_time - last_batch_time >= BATCH_TIMEOUT
            )

            if should_process or current_time - last_batch_time >= MONITOR_INTERVAL:
                try:
                    # バッファ内のプロセスを一度に処理
                    processes_to_process = list(process_buffer)
                    process_buffer.clear()
                    last_batch_time = current_time

                    if processes_to_process:
                        results = analyze_processes_batch(processes_to_process)

                        for process in processes_to_process:
                            pid = process["pid"]
                            is_suspicious = results.get(pid, False)

                            if is_suspicious:
                                if pid not in notified_processes:
                                    print(f"[!] Suspicious process: {process}")
                                    notify(process)
                                    notified_processes.add(pid)
                            else:
                                print(f"[OK] {process['cmd']}")

                except Exception as e:
                    print(f"Error analyzing processes: {e}")

            time.sleep(1)  # CPU負荷を減らすため1秒待機

        except Exception as e:
            print(f"Fatal error: {e}")
            time.sleep(MONITOR_INTERVAL)


if __name__ == "__main__":
    while True:
        try:
            monitor_processes()
        except Exception as e:
            print(f"Fatal error: {e}, restarting in 5 seconds...")
            time.sleep(5)

これを systemd 配下で動作させています

簡単に流れの紹介

  1. ps aux の結果を取得
  2. LLM (gemini) に投げて各プロセスが怪しいか判定してもらう
  3. 怪しい場合は Slack に通知

他のログ監視も同じような仕組みです
株価予測エージェントは上記の流れとだいぶ違うのは違うのですが LLM に過去の株価情報を渡して予測してもらうだけです

感想

以下不正プロセス監視エージェントを使ってみた感想や所感です

お金がかかるので呼び出し方を工夫しなければならない

  • LLM にわたす情報を複数行にまとめて API をコールする
  • 1行ずつだと時間もかかるしお金もかかる
  • ローカル LLM が一つの解決策だがそれなりのマシンスペックは必要

判断するまでの速度

  • API (ネットワーク) + LLM の推論の時間が必ずかかるので判断までに数秒かかる
  • 完全なリアルタイムを実現するのは難しい
  • 一瞬で消えてしまうプロセスをキャッチできないケースがある
  • 速度もローカル LLM で解決できそうだが結局高スペックマシンでもそれなりの推論時間がかかるので数ミリ秒レベルのレスポンス速度は期待できない

精度が曖昧

  • 本当にアウトな場合のテストができない
  • アウトじゃないけどアウトと判定してしまう
  • アウトじゃないプロセスは無視するような仕組みが必要になる (コード内参照)
  • それ(特定のプロセス無視/ホワイトリスト形式)でいいのかという問題もある (同一プロセス名だと攻撃されても検知できなくなる)
  • 前はセーフだったけど次はアウトにしちゃう
  • 「怪しい」という LLM への命令が曖昧すぎるが故の過剰検知とハレーション

そもそもAI エージェントの具体的なユースケースは

  • 人間には難しい「判断」や「解釈」が必要なタスク+状況に応じて行動を変える仕事
  • ログから「人間レベルの洞察」を出す
  • 毎日の情報を「読む → 取捨選択 → 解釈 → 提案」する
  • インシデント予兆検知
  • コードのレポート・改善案
  • などなど、とにかく得意なのは「判断」と「解釈」あと「生成」

(現状は)何かに特化した AI エージェントしか作れない

  • 本当にやりたいことは「何でも」やってくれる汎用 AI エージェント
  • でも現状はやりたいことだけをやってくれる特化 AI エージェントしか作れない
  • もっというと「やりたいこと」すら AI エージェントが見つけて勝手にやってほしい
  • 特化エージェントを組み合わせて汎用っぽく見せかける

AI を使わなくてもいい問題

  • 今回作成した AI エージェントのようなログ監視やプロセス監視の製品はある
  • 判断が静的 (特定の文字列を含む場合にアラート/CPUが90%以上になったらなど) だがそれで十分なケースが多い

そもそも「AI エージェント」の定義は

  • 広義には「人の代わりに作業をやってくれるツール」だと思っている
  • 自分は今回のようなプロセス版のエージェントも VSCode の Copilot Chat のようなエディタもエージェントだと思っている
  • エージェントにもいろいろなタイプがあるので言葉に惑わされてはいけない

既製品を使ってもいい

ブースティング

  • 複数の LLM に判断させてその結果で最終的な判断をする
  • エヴァンゲリオンのマギシステム的なイメージ
  • ただしお金が倍々に増えていくので注意

最後に

AI もあるので AI エージェント自体を作ることは非常に簡単ですが本当に有用なものを作るのは難しいと言うか無くても何とかなるというのが正直なところです

株価予測エージェントは評価機能もあるので評価の結果が良ければ実践投入してどうなるか試してみようかなと思います
売買も自動でできると更に良いかなと思っています