本記事は、KDDIアジャイル開発センター(KAG)Advent Calender2024 11日目の記事です。
はじめに
こんにちは!KDDIアジャイル開発センター株式会社(KAG)のひさふるです。
私は今年4月にこの会社に入社し、8月中旬から実際の案件に配属され毎日スクラム開発を行っています。
そんな私が配属後に困ったことの1つに、ストーリーポイントの見積もりがわからないということがありました。
ストーリーポイントは過去のタスクや基準となるタスクと比較し、見積もるタスクがそれよりどの程度簡単か/難しいかでタスクの大きさを見積もる相対見積の考え方を基準としています。
しかし、新入社員である私は過去のタスクを把握しておらず、タスクで実施する詳細な内容も想像しにくいため、ストーリーポイントの見積もりに慣れるまでとても苦労しました...
そこで、今回は生成AIを使って、ストーリーポイントの見積もりのサポートをしてもらえるツールを作っていこうと思います!
ストーリーポイントとは?
一応、ストーリーポイントについて簡単におさらいしておこうと思います。
ストーリーポイントとは、スクラム開発におけるタスク(=プロダクトバックログアイテム)の大きさや複雑さを表すポイントのことです。
ストーリーポイントの特徴は相対見積もりを行うことで、かかる時間等の絶対的な指標ではなく、過去の同様のタスクや基準となるタスクのポイントと比較し、それよりもタスクが大きいかどうかを相対的に考え、ポイントを割り振っていきます。
また、割り振るポイントにはフィボナッチ数列(1,2,3,5,8,13,21...)を使うことが多いです。
これは、フィボナッチ数列の性質上タスクが大きくなればなるほど隣り合う数字の差が大きくなるため、必然的に選択肢が少なくなり、ポイントの割り振りに迷いにくくなるというメリットがあるためです。
ストーリーポイント見積もりの課題点
ストーリーポイント見積もりは初心者でも見積もりがしやすいよう工夫された手法ではあるものの、いくつか課題点は残っています。
まず、私のように案件に途中から配属された人間の場合、過去どのようなタスクがあったか把握していないため、参考となる情報が少ないという難しさがありました。
また、私のような初心者にはタスクでどのような処理を行えば良いか想像しにくい、といった問題も存在しました。
簡単そうなタスクでも、実はソースコードの大幅な変更が必要な場合があったりと、見積もりにはある程度の知識が必要なことが多い印象です。
生成AIにストーリーポイント見積もりを手伝ってもらおう!
そこで、今回思いついたのは、生成AIを使ってストーリーポイント見積もりを行うという手法です。
もちろん、すべて生成AIにやってもらうことは出来ないので、今回は私のような初心者に対し、参考情報として過去の類似タスクとAIが見積もったポイントを提示する、というところまでを目指しました。
システム概要
今回は過去のタスクをベクトルデータベースに格納し、見積もりたいタスクと類似するタスクを検索出来るRAGシステムをベースに、生成AIに見積もりを行ってもらうプロンプトを投げるプログラムを作成しました。
ベクトルデータベースにはPineconeを使用し、テキストの埋め込みの生成にはOpenAI APIを使用しています。
準備・データ投入
テストデータ準備
今回はRAGシステムの構築ということで、テストデータが必要になります。
外部公開出来るようなバックログが無かったので、今回は生成AIにデータを作ってもらいました。
データ1つ分は以下のような構成となっています。
{
"title": "初回インデキシングパイプライン構築",
"summary": "社内文書を検索対象とする初期インデキシングパイプラインを構築する。",
"storyPoints": 5,
"sprint": 1
}
データに含まれるのはタイトルとサマリ、ストーリーポイントと、タスクが実行されたスプリント番号です。
今回は5スプリント分、計20個程度のタスクを生成しました。
テストデータ投入
まず、Pineconeに登録し、インデックスを作成します。
今回はbacklog-items
という名前でインデックスを作成しました。
埋め込みを生成するモデルはtext-embedding-3-small
を使うことにしました。
次に、データ投入に使用したスクリプトを示します。
長いので折りたたみ
import json
import os
from dotenv import load_dotenv
import openai
from pinecone import Pinecone, PodSpec
load_dotenv()
PINECONE_API_KEY = os.environ.get("PINECONE_API_KEY")
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
openai.api_key = OPENAI_API_KEY
pc = Pinecone(api_key=PINECONE_API_KEY)
index_name = "backlog-items"
dimension = 1536 # text-embedding-3-smallの次元数
index = pc.Index(index_name)
with open("data.json", "r", encoding="utf-8") as f:
data = json.load(f)
completed_items = data.get("completedBacklogItems", [])
vectors_to_upsert = []
for i, item in enumerate(completed_items):
title = item.get("title", "")
summary = item.get("summary", "")
combined_text = f"{title} {summary}"
print(f"({i+1}/{len(completed_items)}) Generating embedding for: {title}")
response = openai.Embedding.create(
model="text-embedding-3-small",
input=combined_text
)
embedding = response["data"][0]["embedding"]
item_id = f"completed_item_{i}"
metadata = {
"title": title,
"summary": summary,
"storyPoints": item.get("storyPoints"),
"sprint": item.get("sprint")
}
vectors_to_upsert.append({"id": item_id, "values": embedding, "metadata": metadata})
if vectors_to_upsert:
print("Upserting vectors to Pinecone...")
index.upsert(vectors=vectors_to_upsert)
print("Upsert completed.")
else:
print("No vectors to upsert.")
print("All done.")
やっていることは簡単で、jsonファイルを読み込み、タスクのタイトルとサマリを結合したものを埋め込みに変換し、Pineconeに格納しているだけです。
生成AI見積もりシステムの作成
それでは、実際に生成AIにストーリーポイントの見積もりをさせてみましょう。
今回は、生成させたテストデータの中から最新のスプリントに含まれるタスクを抽出し、それに対して見積もりを行わせます。
使用したプログラムを以下に示します。
長いので折りたたみ
import json
import os
from dotenv import load_dotenv
import openai
from pinecone import Pinecone
from typing import List
load_dotenv()
PINECONE_API_KEY = os.environ.get("PINECONE_API_KEY")
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
openai.api_key = OPENAI_API_KEY
pc = Pinecone(api_key=PINECONE_API_KEY)
index_name = "backlog-items"
index = pc.Index(index_name)
# data.jsonからデータを読み込み
with open("data.json", "r", encoding="utf-8") as f:
data = json.load(f)
completed_items = data.get("completedBacklogItems", [])
current_items = data.get("currentBacklogItems", [])
def get_embedding(text: str, model: str = "text-embedding-3-small") -> List[float]:
response = openai.Embedding.create(
model=model,
input=text
)
return response["data"][0]["embedding"]
def query_pinecone(embedding: List[float], top_k: int = 2):
# vectorのみで検索
response = index.query(
vector=embedding,
top_k=top_k,
include_metadata=True
)
return response
# completedBacklogItemsからストーリーポイント別の代表例を選定
representative_tasks = {
1: "【1ポイント例】\n- タイトル: ランキングロジックのユニットテスト\n 概要: ランキング計算が期待通り動作しているかを検証するテストコード追加\n (極めて軽微なタスク)",
2: "【2ポイント例】\n- タイトル: 検索結果ページング機能\n 概要: 検索結果をページ分割し、前後ページを閲覧可能にする\n (小規模だが多少の実装が必要なタスク)",
3: "【3ポイント例】\n- タイトル: ドキュメントデータベーススキーマ設計\n 概要: 文書情報を格納するためのDBスキーマ設計や基本的なCRUD実装\n (中程度の複雑性を持つタスク)",
5: "【5ポイント例】\n- タイトル: 初回インデキシングパイプライン構築\n 概要: 社内文書を検索対象とする初期インデキシングパイプラインを構築する\n (大規模で複数工程を含むタスク)"
}
def estimate_story_points(current_title: str, current_summary: str, references: List[dict]):
# 参照アイテム情報をテキスト化
reference_texts = []
for ref in references:
ref_title = ref["metadata"].get("title", "")
ref_summary = ref["metadata"].get("summary", "")
ref_points = ref["metadata"].get("storyPoints", "N/A")
reference_texts.append(
f"- タイトル: {ref_title}\n 概要: {ref_summary}\n ストーリーポイント: {ref_points}\n"
)
reference_section = "\n".join(reference_texts)
# ストーリーポイント代表例テキスト化
representative_text = "\n\n".join([f"{desc}" for sp, desc in representative_tasks.items()])
system_message = {
"role": "system",
"content": "あなたはアジャイル開発におけるストーリーポイントの見積もりアシスタントです。"
}
user_prompt = f"""
あなたにはバックログアイテムのストーリーポイントを見積もってもらいます。
以下は過去の完了済みバックログアイテムで、今回見積もってほしいアイテムと関連性が高いものです。
それぞれのストーリーポイントは、そのタスクの大きさや複雑さを示しています。
【過去の類似タスク】:
{reference_section}
また、以下はストーリーポイント(1,2,3,5)それぞれの完了済みバックログアイテムから抜粋した代表的なタスク例です。
【ストーリーポイント例】:
{representative_text}
これらを踏まえて、以下の未完了アイテムについてストーリーポイントを見積もってください。
【対象アイテム】:
タイトル: {current_title}
概要: {current_summary}
条件:
- ストーリーポイントは1,2,3,5のいずれかで回答してください。
- 回答は以下のフォーマットで出してください:
出力フォーマット:
ストーリーポイント: X
理由: (なぜそのポイントを選んだのか、上記の参照アイテムやストーリーポイント例と比較して重い/軽い等の判断理由を述べてください)
最も参考になりそうな類似タスク:(参考にしたアイテムのタイトルとその内容やストーリーポイントを記述)
すべて日本語で回答してください。
"""
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[system_message, {"role": "user", "content": user_prompt}]
)
return response["choices"][0]["message"]["content"].strip()
for i, item in enumerate(current_items):
title = item.get("title", "")
summary = item.get("summary", "")
combined_text = f"{title} {summary}"
print(f"({i+1}/{len(current_items)}) Embedding current item: {title}")
current_embedding = get_embedding(combined_text)
print("Querying Pinecone for relevant references (vector-only search)...")
search_response = query_pinecone(current_embedding, top_k=2)
references = search_response["matches"]
print(f"Found {len(references)} references. Estimating story points...")
result = estimate_story_points(title, summary, references)
print(f"Estimation result for '{title}':\n{result}\n")
見積もり時にはベクトルデータベースから、見積もり対象に類似するタスクを2件抽出し、参考情報として渡します。
また、各ポイント数に対応する代表的なタスクも参考情報として渡します。
以上を踏まえ、最終的にLLMに渡されるプロンプトの例を示します。
あなたにはバックログアイテムのストーリーポイントを見積もってもらいます。
以下は過去の完了済みバックログアイテムで、今回見積もってほしいアイテムと関連性が高いものです。
それぞれのストーリーポイントは、そのタスクの大きさや複雑さを示しています。
【過去の類似タスク】:
- タイトル: データベースクエリの最適化
概要: 検索パフォーマンス向上のためクエリをチューニング。
ストーリーポイント: 5.0
- タイトル: 検索リクエストのレート制限
概要: スパム的な検索リクエストを制限する機能を実装。
ストーリーポイント: 3.0
また、以下はストーリーポイント(1,2,3,5)それぞれの完了済みバックログアイテムから抜粋した代表的なタスク例です。
【ストーリーポイント例】:
【1ポイント例】
- タイトル: ランキングロジックのユニットテスト
概要: ランキング計算が期待通り動作しているかを検証するテストコード追加
(極めて軽微なタスク)
【2ポイント例】
- タイトル: 検索結果ページング機能
概要: 検索結果をページ分割し、前後ページを閲覧可能にする
(小規模だが多少の実装が必要なタスク)
【3ポイント例】
- タイトル: ドキュメントデータベーススキーマ設計
概要: 文書情報を格納するためのDBスキーマ設計や基本的なCRUD実装
(中程度の複雑性を持つタスク)
【5ポイント例】
- タイトル: 初回インデキシングパイプライン構築
概要: 社内文書を検索対象とする初期インデキシングパイプラインを構築する
(大規模で複数工程を含むタスク)
これらを踏まえて、以下の未完了アイテムについてストーリーポイントを見積もってください。
【対象アイテム】:
タイトル: データベースクエリの最適化
概要: ユーザから検索が遅いというフィードバックを受け検索を高速化する。
条件:
- ストーリーポイントは1,2,3,5のいずれかで回答してください。
- 回答は以下のフォーマットで出してください:
出力フォーマット:
ストーリーポイント: X
理由: (なぜそのポイントを選んだのか、上記の参照アイテムやストーリーポイント例と比較して重い/軽い等の判断理由を述べてください)
最も参考になりそうな類似タスク:(参考にしたアイテムのタイトルとその内容やストーリーポイントを記述)
すべて日本語で回答してください。
これに対する回答がコチラ。
ストーリーポイント: 3
理由: このバックログアイテムはデータベースクエリの最適化に関するタスクであり、過去の類似タスクである
「データベースクエリの最適化」(ストーリーポイント: 5.0)と比較すると、検索の高速化という目標は同じですが、
今回のアイテムはユーザからのフィードバックが遅いという具体的な要望に基づいており、それに対応するための修正
や調整が必要となるため、中程度の複雑性があると考えられます。
最も参考になりそうな類似タスク: 「データベースクエリの最適化」。
このタスクも検索パフォーマンス向上のためのクエリチューニングを行うものであり、
今回のアイテムと同様にデータベース関連の最適化作業を含んでいます。
過去の類似タスクと比較しながら、実行内容の差分を考慮した上で相対的に見積もりが出来ている気がします。
同様に、他のタスクに対しても見積もりをさせた結果が以下のとおりです。
検索システムのユニットテスト
ストーリーポイント: 2
理由: 検索システムのユニットテストは、ランキングロジックのユニットテストと類似しており、それほど大規模でも複雑でもないと考えられます。また、検索結果ページング機能の2ポイント例に近い規模のタスクとも言えます。
最も参考になりそうな類似タスク:
- タイトル: インデキシング処理のユニットテスト
概要: インデキシング機能に対する基本的なユニットテストを追加。
ストーリーポイント: 2.0
(検索システムのユニットテストも、同様に基本的なテストを実施するため、2ポイントが適切と考えられます)
最終リリース準備
ストーリーポイント: 2
理由:
このアイテムは最終リリースの準備作業であり、機能の実装や大規模な変更は含まれず、
主にパッケージ化や提供形式の調整が中心となりそうです。したがって、2ポイントと判断しました。
最も参考になりそうな類似タスク:
- タイトル: 検索結果ページング機能
概要: 検索結果をページ分割し、前後ページを閲覧可能にする
ストーリーポイント: 2.0
このタスクも主に機能の拡張や改善ではなく、既存機能の構成や表示形式の微調整を行うものであり、
規模的に近しいため参考になります。
デプロイ手順ドキュメント化
ストーリーポイント: 2
理由: デプロイ手順ドキュメント化は、実装作業よりも内容を整理してドキュメント化する作業が中心となるため、
作業量が多いわけではないと考えられます。
また、シンプルなタスクであり、他の複雑なタスクと比較して、2ポイントの見積もりが適切だと判断しました。
最も参考になりそうな類似タスク:
- タイトル: 検索結果ページング機能
概要: 検索結果をページ分割し、前後ページを閲覧可能にする
ストーリーポイント: 2
理由: デプロイ手順ドキュメント化も同様に、実装作業よりも内容を整理して構築する作業が中心となります。
そのため、このタスクを参考に2ポイントの見積もりを行いました。
それぞれ、過去の類似タスクを考慮し、見積もりが出来ています。
おわりに
今回はRAGを活用したストーリーポイントの見積もりシステムを構築してみました。
過去のバックログから類似タスクを参照することで、私のような初心者が見積もりを行う際の助けになる可能性は示せたかと思っています。
一方で、実際の見積もりでは、インフラ由来の制約やPOからの要望などにより、見積もり時は様々な要素を考慮しなければなりません。
現在の生成AI技術ではそれら要素をすべて考慮することは難しいので、ストーリーポイントの見積もりはまだまだ人間の仕事となりそうです。