Mirascope って何?
Python の関数を自然言語で書けるライブラリです。
Langchainの代替ライブラリを目指して作られたらしく、Langchainと同じようなことができます。
Mirascope
https://mirascope.com/
たとえば、住所から県名を抜き出す処理を例にとります。
正規表現を使った Python 関数では、以下のように書きます。
import re
from pydantic import BaseModel
class AddressInfo(BaseModel):
prefecture: str # 県名
city: str # 市区町村
def address_to_pref(address_text: str):
"""住所から県名と市区町村を抜き出して返す関数"""
result = re.match(
r"(?P<prefecture>.+(都|道|府|県))(?P<city>.*(市|区|町|村))",
address_text,
)
return AddressInfo.model_validate(result.groupdict())
# 上で定義した関数を実行する
print(address_to_pref("大阪府大阪市中央区1ー1"))
正規表現で抜き出して、Pydanticで結果を検証して、値を返す関数です。
実行すると、以下の Pydantic オブジェクトが返ります。
prefecture='大阪府' city='大阪市中央区'
Mirascope を使うと以下のように書きます。
from mirascope import llm
from pydantic import BaseModel
class AddressInfo(BaseModel):
prefecture: str # 都道府県
city: str # 市区町村
@llm.call(
provider="bedrock",
model="amazon.nova-micro-v1:0",
response_model=AddressInfo,
)
def address_to_pref(address_text: str):
return f"{address_text}から、都道府県と市区町村を抜き出してください"
# 上で定義した関数を実行する
print(address_to_pref("大阪府大阪市中央区1ー1"))
正規表現で書いてあった処理が、自然言語のプロンプトに変わりました。
@llm.call(...) # 使うLLMの設定
def address_to_pref(address_text: str):
return f"{address_text}から、都道府県と市区町村を抜き出してください"
この関数(address_to_pref) の引数、戻り値、挙動は、元の正規表現の関数と全く同じです。
実行すると以下のような Pydantic オブジェクトが返ります。
prefecture='大阪府' city='大阪市中央区'
このようにして、関数をLLMのプロンプトに置き換えることができるライブラリです。
今回使う環境
MirascopeはPython向けのライブラリです。
次のV2では、Python, Typescriptの両方に対応しますが、2025-07-05 現在はまだアルファ版の扱いです。現時点ではPythonだけです。
今回は以下の環境で実装、検証します。
- Python: 3.13
- Mirascope: 1.25.3
- LLMのモデル: Amazon Bedrock - Nova Micro
Nova Microは安くて速いモデルです。
レスポンスの品質的には物足りない部分もありますが、Mirascopeで使うには十分すぎる性能です。
Nova MicroはHaiku 3.5の3倍程度速く、料金は1/20以下です。
※金額が小さすぎて分かりづらいのですが、Claude Haiku 3.5の料金を1とすると、Nova Microの料金は0.04程度です。
参考: 値段と実行速度はクラスメソッドさんがまとめておられるのが分かりやすいです
https://dev.classmethod.jp/articles/amazon-nova-text-generation-model-investigation-aws-reinvent/
Mirascopeを利用する
利用する前に
初めてNova Microを使うのであれば、AWSのマネジメントコンソールのus-east-1(バージニア北部)を開いて、BedrockのモデルアクセスからNova Microへのアクセスを申請しておきます。
プログラムからBedrock のリージョンを指定する
プログラムで実行するときは、リージョンを環境変数で指定します。
ローカルで実行するときは、以下のようにして Bedrock のリージョンを指定します
from os import environ
# AWSのリージョンをus-east-1(バージニア北部)に設定する
# AWS_REGION_NAMEの代わりに、AWS_DEFAULT_REGIONを指定してもいい
environ.setdefault("AWS_REGION_NAME", "us-east-1")
インストールする
以下のようにしてMirascopeをインストールします。
主要な LLM は全て対応していますので、bedrock以外を使うときは、かっこの中を変更してください。
python -m pip install mirascope[bedrock]
実行する
基本的な使い方
ライブラリからllmをインポートします。
from mirascope import llm
プロンプトを返す関数を定義して、llm.callのアノテーションを付けます。
アノテーションの引数にnova-microのIDを指定します。
@llm.call(
provider="bedrock", # プロンプトをBedrockのLLMに実行させる
model="amazon.nova-micro-v1:0" # モデルIDはNova Micro
)
def hello_llm() -> str:
return "こんにちは、Nova"
たったこれだけでLLMの実装は完了です。
この関数を呼び出します。
print(hello_llm())
実行結果は以下の通りです。
こんにちは!お元気ですか?今日はどんなことをお手伝いできるでしょうか?質問や相談があれば、何でも聞いてくださいね。
LLMのモデルを共通化する
複数個所で使うのなら、標準関数のpartialを使って、引数の指定を省略することもできます。
from mirascope import llm
from functools import partial
# デフォルトのLLMを定義する
default_llm_call = partial(llm.call, provider="bedrock", model="amazon.nova-micro-v1:0")
# providerとmodelの指定をデフォルトから参照する
# partialで指定していない引数は、元々のllm.callの引数と同じように指定できる
@default_llm_call(response_model=str)
def hello_llm() -> str:
return "こんにちは、Nova"
また、call_paramsを指定して、API実行時の詳細な引数を指定することもできます。
from mirascope import llm
from mirascope.core.bedrock import BedrockCallParams
from functools import partial
default_llm = partial(
llm.call,
provider="bedrock",
model="amazon.nova-pro-v1:0",
call_params=BedrockCallParams(
inferenceConfig={
"maxTokens": 1000, # トークン数上限を指定する
"temperature": 0.0, # 実行時のレスポンスを固定する
}
),
)
実行に使ったプロンプトやトークン数を見る
response_modelを指定せずに実行すると、実行結果のほかに、トークン数やプロンプト、渡した引数が返ってきます。
@llm.call(provider="bedrock", model="amazon.nova-micro-v1:0")
def hello_llm() -> str:
return "こんにちは、Nova"
print(f"{hello_llm().model_dump_json(indent=2)}")
レスポンスはPydanticのBaseModelを継承したものなので、model_dump_jsonでJSONに変換できます。
実行すると、以下のレスポンスが返ってきます(※レスポンスの一部を抜粋したものです)
{
"messages": [
{
"role": "user",
"content": [
{
"text": "こんにちは、Nova"
}
]
}
],
"call_params": {
"inferenceConfig": {
"maxTokens": 1000,
"temperature": 0.0
}
},
"model": "amazon.nova-micro-v1:0",
"input_tokens": 4,
"output_tokens": 28,
"message_param": {
"role": "assistant",
"content": [
{
"text": "こんにちは!お元気ですか?今日はどんなことをお手伝いできるでしょうか?質問があれば、何でも聞いてください。"
}
]
}
}
精度を上げる
処理の中に数値計算があるのなら、json_modeを有効にしてください。
※json_modeを無効にしていると、途中式を結果だと誤認して結果を返します
@llm.call(
provider="bedrock", # プロンプトをBedrockのLLMに実行させる
model="amazon.nova-micro-v1:0", # モデルIDはNova Micro
json_mode=True # <<< LLMのJSONモードフラグを有効にする
)
def calcurate(a: int, b: int) -> str:
return "{a}に{b}をかけた数値を計算してください"
複雑な処理は、llm.callにtoolsの引数を渡して、ツールに処理させることもできます。
結果を文字列以外の形で受け取る
response_model
の引数に戻り値の型を指定すれば、その形で結果が返ります。
from mirascope import llm
@llm.call(
provider="bedrock",
model="amazon.nova-micro-v1:0",
response_model=int, # 結果を整数で返す
)
def parse_to_arabic(value: str):
return f"{value}を数値に変換して返してください"
# 定義した関数を実行、漢数字をintにする
print(parse_to_arabic("千三百"))
実行するとint型の結果が返ります。千三百を整数で受け取るので、1300が返ってきます。
1300
また、response_modelにPydanticのオブジェクトを指定することで、検証と型変換を同時に実行することもできます。
from mirascope import llm
from pydantic import BaseModel
class Name(BaseModel):
given_name: str # 名
family_name: str # 姓
@llm.call(
provider="bedrock",
model="amazon.nova-micro-v1:0",
response_model=Name,
)
def parse_name(name: str) -> str:
return f"{name}を姓と名に分けてください"
# 定義した関数を実行する
print(parse_name("Kent Beck"))
実行すると、次のように名前が姓と名に分かれて返ってきます。
given_name='Kent' family_name='Beck'
また、Enumを使えば、条件分岐に使うこともできます。
from mirascope import llm
from pydantic import BaseModel
from enum import StrEnum
class Emotion(StrEnum):
HAPPY = "happy" # 幸せ
SAD = "sad" # 悲しい
ANGRY = "angry" # 怒っている
SURPRISED = "surprised" # 驚いている
class Category(BaseModel):
emotion: Emotion # 感情の分類結果
@llm.call(
provider="bedrock",
model="amazon.nova-micro-v1:0",
response_model=Category,
)
def parse_emotion(text: str) -> str:
return f"「{text}」の感情を分類してください"
# 感情の分類を実行する
print(parse_emotion("ずっと欲しかった新刊がやっと買えました"))
実行すると、分類結果がEnum型で返ってきます
emotion=
<Emotion.HAPPY: 'happy'>
# 入力されたテキストを感情分類して、幸せなら
if parse_emotion(input_text) == Emotion.HAPPY:
print("手をたたこう。たんたん")
みたいなことができます。
Mirascopeは何が嬉しい?
LLMを簡単に使えるライブラリですが、簡単に使うのなら「Langchainでいいのでは?」と感じた方もおられるかもしれないです。
MirascopeはLangchainの代替ライブラリであるので、役割に大きな違いはありません。
ただ、大きなメリットは構文です。
使いどころを考えていきます。
1. 高機能なモック関数を作る
たとえば、複雑で厄介な関数を実装する必要があるとします。
モックで固定値を返すと次の処理が上手く動かない。厳密である必要はないけれど、入力に応じた出力をうまく返してほしいケースです。
仕様書と議事録をそのまま Mirascope の関数にします。
from mirascope import llm
from pydantic import BaseModel
from typing import List
from datetime import datetime, timedelta
class DataInfo(BaseModel):
predicted_power_consumption: float # 予測消費電力
class PowerConsumptions(BaseModel):
start_datetime: str # 開始日時
end_datetime: str # 終了日時
predict_result_list: List[DataInfo] # 消費電力データのリスト
@llm.call(
provider="bedrock",
model="amazon.nova-micro-v1:0",
response_model=PowerConsumptions,
)
def request_power_consumption(unit_counts: int, start_datetime: datetime) -> str:
# 日時の計算のようなLLMが不得意な部分はPythonで直接計算して良い
end_datetime = start_datetime + timedelta(minutes=unit_counts + 1)
# 議事録をコピペしただけのプロンプトを返す
return f"""
議事録を参考に、予想した消費電力を返します。
開始日時は {start_datetime} で、終了日時は{end_datetime}です。
{unit_counts} 件の消費電力を予測して、結果として返します。
**議事録**
- 予測した消費電力は30から50の値にしてください、0は来ないです
"""
# 実行、10件分の予測データを出力させる
print(
request_power_consumption(unit_counts=10, start_datetime=datetime(2025, 1, 1, 7, 0))
)
実行すると、引数で指定した10件分の予測データが返ってきます。
貼り付けた議事録にあった通り、30から50の範囲で値が返っています。
start_datetime='2025-01-01 07:00:00' end_datetime='2025-01-01 07:11:00'
predict_result_list=[DataInfo(predicted_power_consumption=30.0), DataInfo(predicted_power_consumption=45.0), DataInfo(predicted_power_consumption=35.0), DataInfo(predicted_power_consumption=40.0), DataInfo(predicted_power_consumption=50.0), DataInfo(predicted_power_consumption=32.0), DataInfo(predicted_power_consumption=48.0), DataInfo(predicted_power_consumption=37.0), DataInfo(predicted_power_consumption=42.0), DataInfo(predicted_power_consumption=47.0)]
精度は出ないので、あくまで一時的なモック関数です。
MirascopeはPythonの関数と同じようにふるまうため、あとから正しい実装に差し替えることができます。
def request_power_consumption(unit_counts: int, start_datetime: datetime) -> PowerConsumptions:
"""正しく実装した関数に置き換える"""
...
return PowerConsumptions(...)
2. 例外系の処理を実装するとき
エラー処理のような、同じ入力に対して必ず同じ出力を返す必要がない処理であれば、Mirascopeをそのまま導入できるのではないかと思います。
Mirascope でエラーログの出力関数を書いてみます。
class ErrorInfo(BaseModel):
error_function_name: str
error_reason: str
is_abort: bool
@llm.call(
provider="bedrock",
model="amazon.nova-micro-v1:0",
response_model=ErrorInfo,
)
def error_log(exception: Exception) -> str:
# スタックトレースを取得する
stack_trace = format_exception(exception)
return f"""
エラーログを受け取りました。このエラーログを解析して、エラーの内容を抽出してください。
<stack_trace>
{stack_trace}
</stack_trace>
<error_message>
{exception}
</error_message>
"""
実行は以下のようにします。
try:
main()
except Exception as e:
print(error_log(e))
たとえば、mainの中で起きた0除算のエラーであれば、以下のようなレスポンスが返ってきます。
error_function_name='main' error_reason='division by zero' is_abort=True
model_validateの中のPydanticの検証エラーであれば、以下のようなレスポンスが返ってきます。
error_function_name='model_validate' error_reason='missing fields' is_abort=True
エラーを共通のレスポンスに詰めることができます。
何をするべきかをEnumで返すように指定しておけば、エラーをどう処理するべきかが分かりやすくなり、トレースやエラー対応の実装がしやすくなります。
3. いっそのこと日本語で Python を書く
Pythonは関数名と変数名を日本語で書けます。
Mirascope を日本語で書いてみます。
from mirascope import llm
from functools import partial
from pydantic import BaseModel
# デフォルトのLLMを定義する
default_llm = partial(llm.call, provider="bedrock", model="amazon.nova-micro-v1:0")
class Output(BaseModel):
合計金額: int
@default_llm(json_mode=True, response_model=Output)
def 個数と単価から合計金額を計算する(単価: int, 個数: int):
return f"""
単価:{単価}円の商品を、個数:{個数}個買った時の合計金額を求めます
単価に個数を掛け算して、その結果を合計金額としてください。
結果として合計金額を返してください。
"""
料金 = 個数と単価から合計金額を計算する(単価=1000, 個数=3)
print(f"{料金}円です")
実行結果は以下のようになります。
合計金額=3000円です
Mirascopeを使うと日本語の文章がそのまま処理になります。
関数名は日本語、呼び出しも日本語とすれば、ほぼすべて日本語になります。
Pythonらしさがなくなり、Python環境上で動かせる疑似言語のような形になります。
さいごに
最近流行っている「自然言語でAIエージェントに指示を出して、プログラムを組んでもらって、修正も自然言語でする」ものとは違って、「Pythonの関数を自然言語で書く」のライブラリです。
Nova Microの速さと安さの恩恵を受けやすく、メンテナンスもしやすいので、ラフスケッチのように「Mirascopeで雑に動く関数を作っておいて、テストを作って、それから真面目に実装する」作業フローができて面白いと感じました。