なにかのまねごと

A Journey Through Imitation and Expression

もし物語に役割があるとするならば

言語化が大事。とてもよく聞く言葉です。
でも私は言語化はそんなに大事じゃないんじゃないかな?と思っています。

ビジネスの文脈で言語化が大事と言われると、それはその通りだろうと思います。仕事に再現性を持たせるためには、手順やそうする理由などを言葉にして、他の人でも、そして未来の自分でも理解できるようにするのは大事だと思っています。
でもそれ以外、特に感情を言葉で扱うときには言語化しすぎると取りこぼすものがあると思っているのです。

強い感情を抱いたときに、言葉にできないと思ったことはないですか?逆に、強い感情を無理にでも言葉にすることで気持ちが落ち着いたことはありませんか?
私はどちらも経験したことがあります。
言葉にできないと思うくらい強く複雑な感情は、無理に言葉にすると情報が欠落します。言葉にするというのは基本的には分かりやすくする作業です。なぜ分かりやすくなるのかというと、複雑な情報を乗せたままにしない、つまり単純化するからです。
だから、強く複雑な感情を無理に言葉にすると、複雑さが削ぎ落とされて単純化され、その感情の強さの元になっていた複雑さが欠落して弱まるのです。
それが強い感情を無理に言葉にしたときに気持ちが落ち着く理由です。

基本的に感情というのは感情そのものを言葉にしても1/3も伝わらないと思った方がいいでしょう。
けれども、感情を伝えるのにとても適したメディアがあります。
それが物語です。
もし物語に役割があるとするならば、それは言葉にできない感情を言葉にしないまま伝えるということだと思います。

これは私の好みも多分に入る話だとは思うのですが、登場人物の感情をそのまま「嬉しかった」「悲しかった」などとそのまま言葉にして説明するのではなく、登場人物が強い感情を持つに至った経緯を描写し受け手に登場人物の感情を追体験させることが、感情を伝えるのにとても適した手段なのだと思います。

ペルソナと向き合うAI記事作成法

「書きたいこと」をそのまま書いても伝わらない、でも

私は何か文章を書くとき、必ず自分が書きたいものを書きます。

そこに想定する読者はいません。だから、大抵はわかる人だけわかってくれたらいいというような文章になります。平たくいうと、ほとんどの読者には届かない文章になります。

しかし最近、AIを使って文章のブラッシュアップに挑戦するようになりました。そのときに大活躍したのがペルソナを設定することでした。

読者の顔を思い浮かべるために有用なのがペルソナ

ペルソナとは、ここでは読者像を指します。想定読者をひとりの人物として捉えることで、伝えたいことの軸は変えずに「書きたい文章」から「伝わる文章」へのリライトが容易になります。筆者の視点が変わると言ってもいいでしょう。

この文章もそうやって書いています。

これがどのくらい「書きたい文章」から「伝わる文章」になったのかはこの文章をアフターとし、そして元の文章をビフォーとして公開します。最下部のおまけからこの文章を書くときに使ったAIチャットを公開していて、その最上部からダウンロードできますので、興味が湧いた方はぜひ読み比べてみて下さい。

なぜAIと協調する文章術にペルソナが効くのか

AIを使うと、タイトルなどの別案を提案してもらったり書かれている内容を要約してもらったりするのは便利にできます。

けれどもそれだけでは「誰に向けた文章なのか」が曖昧になりがちです。

しかしここでAIにペルソナを想定させると、書き手もAIも「ペルソナに届く文章を書く」という同じ目標を共有できるのです。

このペルソナを作ることは、AI自身に言わせると「AI時代の羅針盤になる」そうです。

ペルソナと向き合うための具体的手順

Step1: まず自分で書きたいことを文章に起こす

これは非常に大切です。まずは伝えたいことを含む書きたいことを文章に起こしましょう。

ここではペルソナを具体的に考える必要はないです。自分が書きたいように、でも伝えたいことはこもるように文章を書きます。

Step2: 1の文章をAIに読ませて、ペルソナを作ってもらう

ここでAIに読者のペルソナを作ってもらいます。ペルソナを具体的に作ってもらうと、自分の中で曖昧だった読者像が形を持つようになります。作ってもらったペルソナが想像通りなのか、それとも思っていたのと違うのかの判別は簡単なのです。

もし想定と違ったペルソナが出てきたら、自分の理想の読者像になるまでAIと一緒に練り直しましょう。 その際のプロンプトでは、年齢などの設定にこだわるよりも書きたいことを読んでもらいたい人を想定していくのがいいでしょう。

ある程度方向性を出したら、具体的な肉付けはAIがやってくれます。

Step3: ペルソナはこの記事を読むことで何を知りたいのかを考える

AIとすりわせをしたペルソナができたら、次はAIにペルソナになりきって元の文章を読んでもらい、知りたかったのに欠けていることなどを書き出してもらいます。

そうすることで、書きたいことに欠けていた知りたいことを補うことができます。

全体の構成を直す案をAIに出してもらうのはこのタイミングです。

AIには、ペルソナが知りたい点に答えつつも最初に貼った記事の軸がブレないような文章の構成を考えてもらいます。

Step4: 3で作ってもらった構成を元に自分で記事を書く

この段階ですとペルソナが具体的にいるので、1で書いた文章と同じことを言おうとしても自然と書き方が変わってきます。

この文章を書くときにベースにした文章では、ペルソナを活用した文章の書き方がTipsメモのようになっていました。自分が分かればいいみたいな文章です。

しかし今はペルソナに伝えるつもりで書いているので、このような文章になっています。

Step5: 書き上げた文章を読んだペルソナはどう思うのかをAIを使ってチェックする

最後に書き上げた文章をAIに読んでもらい、ペルソナになりきって読んだときの感想とAI自身の感想を訊きましょう。

これで問題点が見つからなくなるまで、ペルソナになりきって読んだときの感想を訊くのを繰り返しましょう。

けれども、ペルソナ作りが外れていなくて、3で提案してもらった構成もちゃんとしてるのならそんなに問題点が出るようなことにはならないと思います。

ペルソナは架け橋

ペルソナを設定し、AIと共有することで同じ方向を向いて記事のブラッシュアップをすることができます。 また、ペルソナを設定すると筆者の意識も変わり、意識が変わると文章の質も変わります。それは当然、記事の質にも直結します。

まとめると、書き手が伝えたいことを読み手が受け取りたいことに変換するための橋渡しをしてくれるのがペルソナであり、それを設定することでAIとの対話も容易になります。

ペルソナは書き手とAIの橋渡しもしてくれるのです。

ペルソナを設定した記事ライティング、おすすめです。

おまけ

この記事を作るのに使ったチャットを公開します。

https://chatgpt.com/share/686ea8a4-0fcc-8009-a6a1-42f562d46174

また、具体例のStep1にあたる「書きたいこと」を書いた文章は、このチャットの最上部にあります。 ぜひ読み比べてみて下さい。

宝探しに全力だった RubyKaigi 2025

今年は行ってきました RubyKaigi 2025!

そこで STORES さんブースで irb 宝探しをやっていて、今年の RubyKaigi はこれに全力を尽くしました。

https://ruby-quiz-2025.storesinc.tech

あまりにも面白かったので、どうやって解いていったか記録しておきます。でも、読むのに分かりやすいようにある程度編集しています。

もちろんネタバレ全開なので、嫌な人は回れ右ですよー。



このゲームに気づいたのは2日目のこと。面白そうなので早速手を出します。wasm で作られたWeb版 irb にコマンドを打ち込んでいくことで進めていくゲームのようです。

TreasureHunt game on IRB

まず最初に puts "Hello STORES" とあったので、素直に打ち込んでみると、一つ目のお宝ゲット。

次に現在のワークスペースのメソッドや定数、変数等が見れる ls コマンドを打ち込んでみると、インスタンス変数に @stores を発見。試しにこれの中身を見てみると、二つ目のお宝を見つけました。

そしてとりあえず help コマンド打ってみると、TreasureHunt の項目に、th-search コマンドを発見。"Search for treasures in the world." とあるので、このコマンドを打ってみます。出てきたヒントは ls コマンドを打ってみろってことだったので、もうやったなぁと思いもう一度 th-search 。次はこのゲームは誰が提供してるのかというヒント。それもさっき @stores で見つけたなぁ。もう一度 th-search 。そうすると、今度は川に行け、とあるので、 cd River で ls 。すると、River クラスのメソッドなどが見えます。その中に treasure という如何にもなメソッドがあったので実行し、三つ目のお宝をゲット。

この River には色々とあり、次に encoded メソッドを動かしてみると、エンコードされた文字列が。これどうやればデコードできるかなぁと思いつつ、もう一つのメソッド bottom を動かしてみると、今度は Tablet.read を実行しろと。そうしたらあるコードと共に Tablet.interpreter を動かせと。それを実行してみると、先ほどのコードの実行方法とサンプルコードが書いてある。でも、これを機械的に実行する方法がわからないので、まだ与しやすそうな encoded メソッドで出てきた文字列のデコードに取り掛かります。パッとみて解らなかったのが今からすると悔しいですが、あれこれ試して Base64 デコードを試し、四つ目のお宝をゲット。

そして Tablet.read に戻りますが、やっぱり機械的にどうすればいいのかよく分からない。どうコマンドを打てばこのコードが実行できるのかが謎だと思い、これもまだしばらく放置。 th-search を打ってみます。Base64が便利と出てきて、それはさっきやったなと思いもう一度 th-search 。そうしたらアスキーアートと思しき文字列が。IRBで実行できるっぽいのでコピペすると、綺麗な五つ目のお宝をゲット。

それから Tablet の問題に戻ります。で、ふと気づきます。この read と interpreter で出てくる謎コード、 interpreter で書いてある通りに手で演算すればええやん、と。で、そう思ってよくみると Numbers are in base 36 (0-9, a-z) とある。これがまた謎なわけですが、足し算をするからにはアルファベットを数字に直さねば。というわけで Ruby リファレンスの String のページをみて base でページ検索すると to_i メソッドが引っかかります。これかな、と思いつつ、以下のように interpreter にあったコードを実行。

memory = 0
memory += "z".to_i(36)
memory += "z".to_i(36)
memory += "2".to_i(36)
print memory.chr


ここまでやって H と出てきたことで、当たりを掴んだっぽいと確信します。このノリで手動インタープリターを実行し、 interpreter にあったサンプルコードが HELLO にデコードできたのを確認して、 read にあったコードを実行。六つ目のお宝をゲットしました。

次は th-search のヒントに従って Keynote へ。ls を打つと、CP290_TABLE と LOCATION という定数が見えます。CP290_TABLE は0から255までの数字と文字の対照表。 LOCATION を打つと treasure.txt へのパスが書いてあるので、とりあえず File.read を使ってファイルの中身を確認します。

すると、\x で区切られた二桁の16進数が見える。なので、とりあえず16進数のところだけもらうか、と思い、以下を実行。

f = File.read("/home/me/treasure.txt")
s = f.to_s.split("\x")

ところがこれがなぜかエラー。上手くいきません。

これは相当苦戦しましたが、最終的にはなんとなく Ruby リファレンスの File のページを眺め、each_byte を見つけてひらめきます。f.each_byte{ |x| p x } とやってみると255までっぽい数字の並びが!ここまで来ればあとは簡単。以下のスクリプトを実行します。

File.open("/home/me/treasure.txt") do |io|
  ans = []
  io.each_byte { |x| ans << CP290_TABLE[x] }
  ans.join
end

これで七つ目のお宝を見つけました。

それから、これは本当にたまたまだったのですが、このゲーム時々ハングするわけです。なのでリロードをするのですが、見つけたお宝はそのまま状態が保存されています。だったらコードの実行履歴も保存されてないかなぁ、と思ってリロード後に上キーを押してみると…。八つ目のお宝ゲット。これを見つけたのは運がよかったです。

そして th-search は何度も実行するとヒントがループするわけですが、最後のヒントとして th-search --more をやってみろとあるわけです。それをやってみると、TreasureDetector というクラスを発見。早速 cd して ls します。すると hint というメソッドが。実行すると TreasureDetector クラスに nop というクラスメソッドを実装してくれというヒントが。これは何もしないよ、ってことだったので、以下のメソッドを実装。

def self.nop
end

そしてから use コマンドを打ってみると、九つ目のお宝ゲット。

そして他になんかないかなー、と今まで巡ってきたところもよーく見てみると、River クラスに trigger という謎メソッドがあります。そのまま実行してみると引数が必要な様子。でも引数がどうにも分かりません。show_doc してから trigger としても何もドキュメントが出てこない。仕方ないので ls して、今度は @hooks というインスタンス変数に目をつけました。これの中身はハッシュのよう。explore をキーとして値には何かの Proc の配列が入っています。なので、 @hooks[:explore][0].call で十個目のお宝を見つけました。

そして今度は Tablet に移動し、ReverseSide という何かを見つけます。そこへ移動して ls してみると、read と sentence というメソッドが。両方を確認して、これはやるべきことが割とすぐにわかったので、 String#match を使って treasure の部分だけコピペして th-capture に渡しました。これで11個目。今思うと、これにも "You found a treasure!: "って表示する何かがあったのかも知れないですが、それは見つけてないです。

それから、cd IRB で IRB クラス自体も覗いてみました。そこに @irbrc_files というインスタンスメソッドが。これを見てみると .irbrc へのパスがあります。なので、

f = File.read("/home/me/.irbrc")
puts f

してみると、なんかファイルが壊れてるように見えます。なので、これを直すのかなぁと思って手元のエディタで直して File.write してみるのですが何も変わらず。でも、IRBのメソッドを動かしてみると時々動かないものがあって、エラーメッセージの最後に "Maybe IRB bug!" ってあるんです。これを直すのかなぁ、でも irbrc ファイル直してもエラーは変わらんなぁ、って頭を抱えながらKaigi二日目の晩は過ぎて行きました。

翌朝3日目の RubyKaigi 、朝イチで STORES さんのブースに突撃します。そして、ブースにいた方に11個までは自力で見つけたけどあとがわからん、ということで、現在見つけている treasure を見せながらお話をしました。

まず River.trigger の謎ですが、これは引数に :explore を渡してみると、すでに見つけていた @hooks の中身の treasure が出てくるというものでした。

そしてこれ見つけてないみたいですね、と教えてもらったのが、 th-search で出てくるヒントのうち、この問題は誰が提供しているかというやつ。これはもう @stores で見つけたのでは?と思いながら、 STORES と打ち込んでみると…12個目のお宝発見。マジかー!!ってなりました。

最後の一つは irb についてとても詳しくないと見つからないという話だったのですが、 .irbrc が壊れてるのを直しても変わらないという話をしたら、それが壊れて見えるのは irb の表示の問題ですという返答でした。え、じゃあ13個目のお宝はどこに?となったのですが、そこは自分で探してみてください、ちなみにゲーム中でもノーヒントです、というご返答をいただきました。

なので頑張ってみた結果、その日の夜になってようやくコマンドのサジェストから cd TreasureHunt ができることを発見し、TreasureHunt クラスへ行ってみたところ、TREASURE_DIGESTS という定数を発見。みてみると、13個の配列に暗号化された文字列が入っています。これが答えか、最後の一つをデコードするだけだ!と思ってツイートし、就寝。ちなみに、正当な見つけ方はこの時点でもう諦めてます。

そしてDay4、帰りの新幹線でまたデコードに取り組みます。暗号化された文字列は0からfまでの16個の数字が使われていて、どれも同じ桁数で64桁です。で、ネットであれこれ調べて64桁は SHA256 ですね、ということを発見。なので、最初に見つけていたお宝、 ST-HELLO を使って Digest::SHA256.hexdigest("ST-HELLO") とやってみると TREASURE_DIGESTS の最初の文字列に一致します。おっし、ということで、ついでにあれこれやっているうちに発見していた手法、 game = new をして、変数 game に TreasureHunt のインスタンスを代入します。そこから、game.treasures とやると今までに見つけたお宝たちが配列で読み取れるので、以下のスクリプトを実行します。

gems = game.treasures.dup
TREASURE_DIGESTS.reject{|td| gems.map{|gem| Digest::SHA256.hexdigest(gem)}.include?(td)}

そうすると、最後のお宝の暗号化文字列が手に入ります。

あとは、フォーマットを意識しつつブルートフォースをして答えを見つけ出すだけです。

もしお宝の文字列に絵文字なんかが絶対に入っていたら非常に辛かったですがそれはないだろうと予想して、AA-AAA みたいな文字列から順々に SHA256 エンコードして最後の暗号化文字列と比較していって、無事に最後のお宝も見つけました。ここのコードは割愛。

そうして現れた、Congratulations!

Congratulations!

最後はズルもしましたが、ゲームクリアはやっぱり嬉しいですね。

STORES さんの解説ブログ、楽しみにしています。