今回は、OpenAI Codex (以下Codex)を用いて、自動的にプログラムを作ってもらいました。
自分では、一切コードを修正をしていません。
Codexとは
自然言語を理解し、コードを書くAIが、Open AI Codexです。プログラミング学習から実務的なコード補助まで幅広く活用できます。
チャレンジしたこと
自宅パソコンにインストールされているローカルLLMと通信をして、ローカルLLMと英会話をするPythonプログラム
自宅パソコンスペック
- OS : Ubuntu 24.04.2 LTS (Noble Numbat)
- CPU : AMD Ryzen 7 5700X
- GPU : NVIDIA GeForce RTX 5060 Ti
- メモリ : 48GB
環境構築
自宅パソコンにローカルLLMをインストールする
-
- 今回は「qwen3:14b-q4_K_M」を使用しています。
- 詳細なインストール方法は、公式サイトを確認してください。
インストールが正常終了した後、下記コマンドを実行すると現在インストールされているローカルLLM一覧が
表示されます。
ollama list NAME ID SIZE MODIFIED qwen3:14b-q4_K_M bdbd111c22d3 9.3 GB 33 hours ago -
下記コマンドを実行して、ローカルLLMを起動します。
ollama run モデル名
Codexの設定
- Codexのインストール
npm install -g @Openai/codex - 初回起動・設定
Webブラウザでログインを介して認証ができます。
Codex
Codexでは、~/.codex/AGENTS.mdに共通プロンプトを記述すると、全ての会話に自動で読み込まれます。~/.codex/AGENTS.md - VSCODEに拡張機能のインストールをします。(vscodeの拡張機能からインストールをお願いします。)
実際にプロンプトを渡して作ってもらってみた
プロンプト内容 : 自分のパソコンにインストールされているqwen(ローカルllm)と通信をして英会話ができるpythonアプリを生成して。
下記回答内容
実装内容
- qwen_client.py:1 にローカルで稼働するQwenのOpenAI互換エンドポイントへ安全にリクエストするクライアント実装とエラーハンドリングを追加しました。
- conversation.py:18 に英会話向けのシステムプロンプトと履歴管理を行うConversationSessionを実装し、生成パラメータを指定して送信できるようにしました。
- main.py:12 にCLIエントリーポイントを用意し、会話ループ、補助コマンド(:exit, :reset, :help)やトピック指定オプションを実装しました。
- README.md:1 に環境変数設定や利用手順、トラブルシュートを含むセットアップガイドを追加しました。
- requirements.txt:1 に必要な依存関係としてrequestsを明示しました。
確認
- python3 -m compileall . を実行し、主要モジュールの構文チェックを通過しました。
次のステップ
- pip install -r requirements.txt を実行後、python3 main.py --topic "travel in New York" などでローカルQwenサーバーに接続し、応答を確認してください。
# conversation.py
"""英会話セッション管理モジュール。"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import List, Sequence
from qwen_client import ChatMessage, QwenClient, QwenClientError
BASE_PROMPT = (
"You are an encouraging English conversation partner. "
"Keep replies in English, use natural expressions, and politely correct "
"any important mistakes with short explanations. "
"Finish each reply with a follow-up question to keep the conversation going."
)
@dataclass
class ConversationSession:
"""Qwenとの英会話セッションを管理する。"""
client: QwenClient
system_prompt: str = BASE_PROMPT
history: List[ChatMessage] = field(default_factory=list)
def __post_init__(self) -> None:
if not self.history:
self.history.append(ChatMessage("system", self.system_prompt))
def reset(self, extra_instructions: str | None = None) -> None:
self.history = [ChatMessage("system", self._build_prompt(extra_instructions))]
def _build_prompt(self, extra: str | None) -> str:
if extra:
return f"{self.system_prompt}\nAdditional context: {extra.strip()}"
return self.system_prompt
def send(
self,
user_text: str,
*,
temperature: float = 0.7,
top_p: float = 0.8,
max_tokens: int | None = None,
) -> str:
user_message = ChatMessage("user", user_text.strip())
self.history.append(user_message)
try:
assistant_reply = self.client.chat(
self.history,
temperature=temperature,
top_p=top_p,
max_tokens=max_tokens,
)
except QwenClientError:
self.history.pop()
raise
self.history.append(ChatMessage("assistant", assistant_reply))
return assistant_reply
def get_history(self) -> Sequence[ChatMessage]:
return tuple(self.history)
# main.py
"""ローカルQwenと英会話を行うCLIアプリケーション。"""
from __future__ import annotations
import argparse
import sys
from textwrap import dedent
from conversation import ConversationSession
from qwen_client import QwenClient, QwenClientError
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="ローカルで稼働するQwenと英会話を行う簡易CLIアプリ",
)
parser.add_argument(
"--temperature",
type=float,
default=0.7,
help="生成多様性。0.0〜1.0の範囲で調整してください",
)
parser.add_argument(
"--top-p",
type=float,
default=0.8,
help="nucleus samplingの上限確率",
)
parser.add_argument(
"--max-tokens",
dest="max_tokens",
type=int,
default=None,
help="応答の最大トークン数 (未指定ならモデル依存)",
)
parser.add_argument(
"--topic",
type=str,
default=None,
help="会話のテーマやシーンを英語で指定すると最初の指示に追加されます",
)
return parser
def print_intro() -> None:
intro = dedent(
"""
Qwen English Conversation CLI
--------------------------------
:exit で終了 / :reset で会話リセット / :help で使い方表示
英語または日本語で入力してください (応答は英語になります)。
"""
).strip()
print(intro)
def print_help() -> None:
help_text = dedent(
"""
利用可能なコマンド:
:exit アプリを終了
:reset 会話履歴をクリアし初期状態に戻す (topic指定があれば再適用)
:help このヘルプを表示
通常の入力はそのままQwenへ送信されます。
"""
).strip()
print(help_text)
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
client = QwenClient()
session = ConversationSession(client)
if args.topic:
session.reset(extra_instructions=args.topic)
print_intro()
if args.topic:
print(f"[Topic] {args.topic}")
while True:
try:
user_input = input("You > ").strip()
except (EOFError, KeyboardInterrupt):
print("\n終了します。ありがとうございました。")
return 0
if not user_input:
continue
lowered = user_input.lower()
if lowered in {":exit", ":quit"}:
print("終了します。ありがとうございました。")
return 0
if lowered == ":help":
print_help()
continue
if lowered == ":reset":
session.reset(extra_instructions=args.topic)
print("会話履歴をリセットしました。")
continue
try:
reply = session.send(
user_input,
temperature=args.temperature,
top_p=args.top_p,
max_tokens=args.max_tokens,
)
except QwenClientError as exc:
print(f"[Error] {exc}")
continue
print("Qwen >", reply.strip())
def entrypoint() -> None:
sys.exit(main())
if __name__ == "__main__":
entrypoint()
# qwen_client.py
"""Qwenクライアントモジュール。ローカルで稼働するQwen/Ollamaサーバーと通信する。"""
from __future__ import annotations
import os
from dataclasses import dataclass
from typing import Any, Dict, Iterable, List, Optional
import requests
class QwenClientError(RuntimeError):
"""Qwenクライアントでエラーが発生した場合に送出する例外。"""
@dataclass
class ChatMessage:
role: str
content: str
def to_dict(self) -> Dict[str, str]:
return {"role": self.role, "content": self.content}
class QwenClient:
"""
ローカルのOpenAI互換 or OllamaネイティブAPIを自動判別して叩くクライアント。
- 優先: POST {base}/v1/chat/completions (OpenAI互換)
- フォールバック: POST {base}/api/chat (Ollamaネイティブ)
"""
def __init__(
self,
base_url: Optional[str] = None,
api_key: Optional[str] = None,
model: Optional[str] = None,
timeout: float = 60.0,
) -> None:
# 既定は Ollama のポート 11434
self.base_url = (base_url
or os.environ.get("QWEN_BASE_URL")
or "http://127.0.0.1:11434")
self.api_key = api_key or os.environ.get("QWEN_API_KEY") or "ollama"
self.model = model or os.environ.get("QWEN_MODEL") or "qwen3:14b-q4_K_M"
self.timeout = timeout
self._v1_url = self._join(self.base_url, "/v1/chat/completions")
self._ollama_url = self._join(self.base_url, "/api/chat")
@staticmethod
def _join(base: str, path: str) -> str:
return base.rstrip("/") + path
def chat(
self,
messages: Iterable[ChatMessage],
temperature: float = 0.7,
top_p: float = 0.8,
max_tokens: Optional[int] = None,
) -> str:
# --- 1) OpenAI互換 (/v1/chat/completions) を試す ---
payload_v1: Dict[str, Any] = {
"model": self.model,
"messages": [m.to_dict() for m in messages],
"temperature": temperature,
"top_p": top_p,
"stream": False,
}
if max_tokens is not None:
payload_v1["max_tokens"] = max_tokens
headers = {"Content-Type": "application/json"}
# OpenAI互換では Authorization: Bearer があると嬉しい実装が多い
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
try:
resp = requests.post(self._v1_url, json=payload_v1, headers=headers, timeout=self.timeout)
if resp.status_code == 200:
return self._parse_openai_style(resp.json())
# 互換未対応っぽいステータスならフォールバック
if resp.status_code in (404, 400, 501):
# 落ちずにフォールバックへ
pass
else:
# 明確なサーバーエラーなどはここで報告
raise QwenClientError(
f"OpenAI互換APIの呼び出しに失敗: status={resp.status_code} body={resp.text}"
)
except requests.RequestException as exc:
# 接続不能など→フォールバックへ
pass
# --- 2) Ollamaネイティブ (/api/chat) フォールバック ---
payload_ollama: Dict[str, Any] = {
"model": self.model,
"messages": [m.to_dict() for m in messages],
"stream": False,
# Ollamaは temperature/top_p を受け付ける
"options": {"temperature": temperature, "top_p": top_p},
}
try:
resp = requests.post(self._ollama_url, json=payload_ollama, headers={"Content-Type": "application/json"}, timeout=self.timeout)
except requests.RequestException as exc:
raise QwenClientError(f"Qwen/Ollamaサーバーへの接続に失敗しました: {exc}") from exc
if resp.status_code != 200:
raise QwenClientError(
f"OllamaネイティブAPIの呼び出しに失敗: status={resp.status_code} body={resp.text}"
)
try:
data = resp.json()
except ValueError as exc:
raise QwenClientError("サーバーからJSON以外の応答を受信しました") from exc
# Ollama形式をパース
return self._parse_ollama_style(data)
@staticmethod
def _parse_openai_style(data: Dict[str, Any]) -> str:
try:
return data["choices"][0]["message"]["content"]
except (KeyError, IndexError, TypeError) as exc:
raise QwenClientError("OpenAI互換API: 想定外の応答形式です") from exc
@staticmethod
def _parse_ollama_style(data: Dict[str, Any]) -> str:
"""
代表的なOllama /api/chat 応答:
{
"model": "...",
"created_at": "...",
"message": {"role": "assistant", "content": "..."},
"done": true
}
"""
# 単発応答(stream=False)
if isinstance(data, dict) and "message" in data and isinstance(data["message"], dict):
content = data["message"].get("content")
if isinstance(content, str):
return content
# 万一将来の形式変化に備えて最低限の保険
if isinstance(data, dict) and "choices" in data:
try:
return data["choices"][0]["message"]["content"]
except Exception:
pass
raise QwenClientError("OllamaネイティブAPI: 想定外の応答形式です")
実際に動かしてみた
~/project/python_talking_english$ python3 main.py
Qwen English Conversation CLI
:exit で終了 / :reset で会話リセット / :help で使い方表示
英語または日本語で入力してください (応答は英語になります)。
You > Hello!
Qwen >
Okay, the user said "Hello!" so I need to respond in a friendly way. Let me start with a greeting. Maybe "Hi there! How are you doing today?" That sounds natural. Now, I should check if there are any grammar mistakes. The user's message is correct, so no need to correct anything. Next, I need to ask a follow-up question to keep the conversation going. Maybe ask about their day or if they have any plans. Let me make sure the tone is encouraging and polite. Alright, that should work.
Hi there! How are you doing today? 😊
I hope you're having a great day! What have you been up to lately?
You > Lately, I've been working hard studying for IT certification exams! October is the big day, so I'm putting in that final push!
Qwen >
Okay, the user is preparing for IT certification exams in October. They mentioned they're working hard and in the final push. I need to acknowledge their effort and maybe offer some encouragement or tips.
First, I should congratulate them on their dedication. Then, maybe ask how they're managing their study time. It's good to show interest in their methods. Also, I should check if they need any advice or resources. But I have to be careful not to overstep. Maybe suggest time management techniques or stress relief, but keep it open-ended.
Wait, the user might be looking for validation. So, reinforcing their hard work is important. Also, asking about their study plan can help them articulate their approach. Maybe they have specific challenges they're facing. I should keep the tone positive and supportive. Let me make sure the response is encouraging and leads to a follow-up question that's open-ended.
That’s amazing! You’re so dedicated—congratulations on your hard work! 🎉 October seems like a big goal, but I bet you’re already making great progress. How are you balancing your study time with other responsibilities?
P.S. If you ever want tips on staying focused or managing stress, I’d love to help! 😊
You >
感想
なかなかいい感じに実装をしてくれました!
今回、自分は何もコードを触っていないですし、中身のチェックもしていません。
しかし、所見でこれだけ的確に動くものを実装してくれるなら、簡単なプログラムとかは作らせてもいいかもしれないですね!
反面、これをプログラムの勉強ツールとして使うのはちょっと向かないかなと思いました。
自動的に作ってくれるので、基本的に人間は関与しません。なので、プログラムを理解できる人が、時間短縮や効率化のためにやるならとても素晴らしいツールだとは思いますが、プログラム初心者がプログラムの勉強として使うツールとしては、そもそも自分でコードを書かないので、あまり向かないのかなっていうのが率直な感想です。
ですが、今回作ってもらった英会話アプリは、自分の会話にも的確に回答をしてくれるし、間違えた箇所があれば指摘もしてくれる。
今後は、翻訳内容や単語の保存機能などを追加実装していけばchatGPTより優秀な英語勉強ツールになりそうな予感!
また進捗があれば投稿します。