社内コードベース向けAI(RAG)を最速で構築する手順

目的

社内アプリのソースコードを対象としたAI(RAG)を最速(30分以内)で構築します。
  • 質問→ベクトル検索→LLM→回答(+シーケンス図作成)

前提条件

  • VM(Ubuntu 24.04)
  • Docker が動作
  • 環境変数 OPENAI_API_KEY.env に設定
echo OPENAI_API_KEY=sk-… > ~/.env

1. Python 仮想環境&ライブラリ整備

python3 -m venv venv && source venv/bin/activate && pip install chromadb==0.6.3 langchain-chroma==0.2.3 langchain-openai==0.3.14 fastapi uvicorn python-dotenv streamlit requests

2. ベクトルインデックス生成

ソースコードリポジトリをファイル走査して 1,000~1,200文字チャンクに分割し、OpenAI で埋め込み→Chroma に保存。
対象が数千ファイルであれば、実行時間は十数分、コストは$1.5程度でした。
実行例:python create_index.py --repo ~/repo1 --persist ~/chroma-data --col repo1
#!/usr/bin/env python3
"""
create_index.py — リポジトリを Chroma インデックス化
"""
import argparse,os,pathlib,shutil,sys
from dotenv import load_dotenv
import openai
from langchain.text_splitter import RecursiveCharacterTextSplitter,Language
from langchain.docstore.document import Document
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma

p=argparse.ArgumentParser()
p.add_argument('--repo',required=True)
p.add_argument('--persist',default='~/chroma-data')
p.add_argument('--col',default='repo1')
p.add_argument('--chunk',type=int,default=1200)
p.add_argument('--overlap',type=int,default=100)
args=p.parse_args()

ROOT=pathlib.Path(args.repo).expanduser().resolve()
PERSIST=pathlib.Path(args.persist).expanduser().resolve()/args.col
load_dotenv()
openai.api_key=os.getenv('OPENAI_API_KEY') or sys.exit('Missing API key')

EXTS={'.php':Language.PHP,'.ts':Language.TS,'.js':Language.JS,
      '.vue':Language.JS,'.twig':Language.HTML,
      '.scss':Language.CPP,'.css':Language.CPP}
files=[f for f in ROOT.rglob('*') if f.suffix in EXTS]
docs=[]
for f in files:
    try: txt=f.read_text(errors='ignore')
    except: continue
    docs.append(Document(page_content=txt,metadata={'path':str(f.relative_to(ROOT))}))
splits=[]
for d in docs:
    lang=EXTS[pathlib.Path(d.metadata['path']).suffix]
    splits+=RecursiveCharacterTextSplitter.from_language(
        language=lang,chunk_size=args.chunk,chunk_overlap=args.overlap
    ).split_documents([d])

if PERSIST.exists(): shutil.rmtree(PERSIST)
emb=OpenAIEmbeddings(model='text-embedding-3-large')
Chroma.from_documents(documents=splits,embedding=emb,
    persist_directory=str(PERSIST),collection_name=args.col)
print('Indexed at',PERSIST)

3. 質問応答 API 整備

FastAPI で /ask(QA)と /render_mermaid(Mermaid→PNG)を提供。
クエリパラメータでリポジトリ(repo1/repo2)とモデル(gpt-4.1/gpt-4.1-mini/gpt-4o-mini/o4-mini)を切替可能です。
起動例:uvicorn qa_api:app --host 127.0.0.1 --port 8600 --log-level debug
#!/usr/bin/env python3
"""
qa_api.py — QA(+Mermaid→PNG) API(repo/model切替可)
"""
import pathlib,uuid,subprocess,uvicorn
from fastapi import FastAPI,HTTPException,Query
from fastapi.responses import FileResponse
from pydantic import BaseModel
from dotenv import load_dotenv
from langchain_openai import OpenAIEmbeddings,ChatOpenAI
from langchain_chroma import Chroma
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

PERSIST_BASE=pathlib.Path.home()/'chroma-data'
REPOS=['repo1','repo2']
MODELS=['gpt-4.1','gpt-4.1-mini','gpt-4o-mini','o4-mini']
TMP=pathlib.Path('/tmp/mermaid_pngs');TMP.mkdir(mode=0o777,parents=True,exist_ok=True)
DOCKER_IMG='minlag/mermaid-cli';SCALE='3'
EMB='text-embedding-3-large';TOP_K=8
load_dotenv()

emb=OpenAIEmbeddings(model=EMB)
RETRIEVERS={r:Chroma(persist_directory=str(PERSIST_BASE/r),
    collection_name=r,embedding_function=emb
).as_retriever(search_kwargs={'k':TOP_K}) for r in REPOS}

prompt=PromptTemplate(input_variables=['context','question'],template="""
You are a code expert. Context:
{context}

Question:
{question}

1. Answer concisely.
2. If sequence diagram is requested, include a Mermaid sequenceDiagram block.
3. Otherwise, no Mermaid.
""")

app=FastAPI()
class QAReq(BaseModel):question:str

@app.post('/ask')
def ask(req:QAReq,
        repo:str=Query('repo1',enum=REPOS),
        model:str=Query('gpt-4.1',enum=MODELS)):
    retr=RETRIEVERS.get(repo)
    if not retr: raise HTTPException(400,'Unknown repo')
    if model in ['gpt-4.1','gpt-4.1-mini','o4-mini']:
        llm=ChatOpenAI(model_name=model)
    else:
        llm=ChatOpenAI(model_name=model,temperature=0.0)
    chain=RetrievalQA.from_chain_type(llm=llm,chain_type='stuff',
        retriever=retr,chain_type_kwargs={'prompt':prompt},
        return_source_documents=True)
    res=chain(req.question)
    return {'answer':res['result'],
            'sources':[d.metadata['path'] for d in res['source_documents']]}

class RenderReq(BaseModel):mermaid:str

@app.post('/render_mermaid')
def render(req:RenderReq):
    k=uuid.uuid4().hex
    m=TMP/f'{k}.mmd';p=TMP/f'{k}.png'
    m.write_text(req.mermaid,encoding='utf-8')
    try:
        subprocess.run(['docker','run','--rm','-v',
            f'{TMP.absolute()}:/data',DOCKER_IMG,
            '-i',f'/data/{k}.mmd','-o',f'/data/{k}.png',
            '-s',SCALE],check=True,stdout=subprocess.PIPE,
            stderr=subprocess.PIPE)
    except subprocess.CalledProcessError as e:
        err=(e.stderr or e.stdout).decode().strip()
        raise HTTPException(500,detail=f'Mermaid error: {err}')
    return FileResponse(str(p),media_type='image/png')

@app.get('/health')
def health(): return {'status':'ok'}

if __name__=='__main__':
    uvicorn.run(app,host='0.0.0.0',port=8600)

4. Web UI 整備

起動例:streamlit run ui.py --server.port 8501
#!/usr/bin/env python3
"""
ui.py — Streamlit チャット UI(repo/model選択+PNG表示)
"""
import os,re,requests,streamlit as st
from dotenv import load_dotenv

st.set_page_config(page_title='コードアシスタント',layout='wide')
load_dotenv()
API_ASK=os.getenv('API_ASK','http://localhost:8600/ask')
API_RDR=os.getenv('API_RENDER','http://localhost:8600/render_mermaid')

repo=st.sidebar.selectbox('リポジトリ',['repo1','repo2'],index=0)
model=st.sidebar.selectbox('モデル',['gpt-4.1','gpt-4.1-mini','gpt-4o-mini','o4-mini'],index=0)
st.sidebar.markdown(f'ask: `{API_ASK}`')
st.sidebar.markdown(f'render: `{API_RDR}`')

MERMAID_RE=re.compile(r'```mermaid\n(.*?)```',re.S)
st.title('🛠️ コード QA')

if 'chat' not in st.session_state: st.session_state.chat=[]

for r,m in st.session_state.chat: st.chat_message(r).write(m)
q=st.chat_input('質問を入力...')
if q:
    st.session_state.chat.append(('user',q));st.chat_message('user').write(q)
    with st.spinner('回答中…'):
        res=requests.post(f"{API_ASK}?repo={repo}&model={model}",
                          json={'question':q}).json()
        ans,sources=res['answer'],res['sources']
        st.session_state.chat.append(('assistant',ans))
        with st.chat_message('assistant'):
            parts=MERMAID_RE.split(ans)
            for i,p in enumerate(parts):
                if i%2==0 and p.strip(): st.markdown(p)
                if i%2==1:
                    img=requests.post(API_RDR,json={'mermaid':p}).content
                    st.image(img,use_container_width=True)
                    with st.expander('Mermaid ソース'): st.code(p)
            if sources:
                with st.expander(f'{len(sources)} 件の参照コード'):
                    for s in sources: st.markdown(f'- `{s}`')

5. Nginx 設定(抜粋)

location /api/ {
    proxy_pass http://127.0.0.1:8600/;
}
location / {
    proxy_pass http://127.0.0.1:8501;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

6. ブラウザで動作確認

ブラウザで / を開き、「ログイン処理の概要やシーケンス図を生成して」など質問すると、回答+シーケンス図が得られます。

今後の予定

自動コードレビューや、エディタ内でのコード補完など。