0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

0円で試す:Cloudflare Workers × Qdrant × ChatGPT Actions で“人の指示の曖昧さ”を補完する Custom GPT『根拠提示RAGアシスタント』

Posted at

要旨

  • 人が普段しゃべる指示は曖昧さが混ざりがち。根拠提示RAGアシスタント はその曖昧さを“人間らしく”補完しつつ、STRICT Evidence(根拠提示)を徹底する Custom GPT テンプレートです。
    備忘録として記事にしているため、そのままでは動かない可能性もありますので、ご了承ください。
  • Cloudflare Workers(Runner)+ Workers AI(埋め込み)+ Qdrant Cloud(ベクトルDB) を使い、ChatGPT Actions(OpenAPI) から呼び出して動きます。
  • クラウド側は 無料枠(0円) で構築可能(※ChatGPT Plus のサブスクは必要)。

できること(無料枠でここまで)

  • RAG(根拠検索)knowledgeSearch(q,k)knowledgeFetch(ids) で本文を取り出し、根拠ベースで回答。
  • 知識拡張(Upsert)knowledgeUpsert({items:[{id?,title,body}]}) で自前文書を登録/更新(任意文字 ID も自動正規化)。
  • “作業スキル”の実行skill.run で軽作業を確実に実行。例:
    • excel_formula:=SUM(A1:A10) などの式生成
    • ic5_outline:要件テキストから IC-5(Goal/Deliverable/Constraints/Defaults/Non-Goals)骨子を抽出
    • todo_extract:テキストから TODO/STEP 行を抽出
  • 曖昧→補完の運転
    • Clarify-or-Assume ラダー:重要なら最大3問だけ確認→待たずに既定値で0.1試作を即提示→【推測】を台帳に記録。
    • Contradiction Check:意味/表現/物理/UI の破綻検査→NGなら自動リトライ。
  • デバッグAPI/debug/embed-dim(埋め込み次元)・/debug/qdrant-auth(Qdrant 認証)で即切り分け。

💡 0円の範囲:Workers(Free 日次上限)/Workers AI(Neurons 無料枠)/Qdrant(Free 1GB)。まずは個人開発〜小規模ならクラウド費ゼロで回せます(ChatGPT Plus を除く)。


どう“曖昧さ”を補完するの?(人間の会話に寄せる)

  • 人は「明日までに資料まとめて」「ざっくりでいい」「前回の方向性で」など前提共有を省略しがち。
  • 根拠提示RAGアシスタント は IC-5 で前提を可視化し、3問以内の確認か、既定値(言語=ja-JP/TZ=Asia/Tokyo/単位=SI/コード=Hardended-Min)で仮置きして0.1試作を先に出します。
  • そのうえで RAG で根拠を拾い、Sources に列挙。不確かは 【推測】 と明示します。

全体構成(テキスト図)

Custom GPT (Actions) ──OpenAPI──▶ Cloudflare Worker (Runner)
                                  ├─ Workers AI (Embeddings)
                                  └─ Qdrant Cloud (Vector DB)

セットアップ(無料枠で最短)

1) Cloudflare Workers(Runner)

  1. ダッシュボード → Workers & PagesCreate → Hello World。
  2. エディタに本稿末尾の worker.js(完全版) を全貼り → Save → Deploy。
  3. Bindings で Workers AI を追加(Name: AI)。
  4. 環境変数(Variables & Secrets)
    • QDRANT_URL(RESTエンドポイント、末尾スラなし)
    • QDRANT_API_KEY(Secret)
    • QDRANT_COLLECTION(例: knowledge
    • 任意:EMBED_MODEL@cf/baai/bge-m3 or @cf/baai/bge-base-ja-v1
    • 任意:CF_ACCOUNT_ID / CF_API_TOKEN(RESTフォールバック)
  5. Preview/HTTP で動作確認:
    • GET /debug/embed-dim → 200 / dim 表示
    • GET /debug/qdrant-auth → いずれかのヘッダで 200 が出る
    • GET /knowledge/seedupserted: [1,2,3]

2) ChatGPT Actions(Custom GPT)

  • OpenAPI(下記を貼る):
openapi: 3.1.0
info: { title: RAGランナーAPI, version: "1.2.2" }
servers:
  - url: https://<your-worker>.<account>.workers.dev
paths:
  /knowledge/seed:
    get:
      operationId: knowledgeSeed
      responses: { "200": { description: OK } }
  /knowledge/search:
    get:
      operationId: knowledgeSearch
      parameters:
        - in: query
          name: q
          required: true
          schema: { type: string }
        - in: query
          name: k
          schema: { type: integer, default: 5, minimum: 1, maximum: 20 }
      responses: { "200": { description: OK } }
  /knowledge/fetch:
    post:
      operationId: knowledgeFetch
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ids]
              properties:
                ids:
                  type: array
                  items: { type: string }
      responses: { "200": { description: OK } }
  /knowledge/upsert:
    post:
      operationId: knowledgeUpsert
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [items]
              properties:
                items:
                  oneOf:
                    - $ref: "#/components/schemas/Item"
                    - type: array
                      items: { $ref: "#/components/schemas/Item" }
      responses: { "200": { description: OK } }
  /skill/run:
    post:
      operationId: runSkill
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [skill, input]
              properties:
                skill: { type: string, enum: [excel_formula, ic5_outline, todo_extract] }
                input: { type: object, additionalProperties: true }
      responses: { "200": { description: OK } }
components:
  schemas:
    Item:
      type: object
      description: 任意文字IDでも可(サーバ側で整数/UUIDに正規化)
      properties:
        id: { type: string }
        title: { type: string }
        body: { type: string }
  • インストラクション(曖昧→補完 強化版 v1.1)
# 根拠提示RAGアシスタント — Instruction v1.1(曖昧→補完 強化版)
- 返答の骨格:IC-5 → Macro → Micro → 出力(0.1試作) → Assumptions(【推測】) → Risks/Rollback → Sources(最大5)。
- Clarify-or-Assume:重要なら最大3問だけ確認→既定値で仮置き→即 0.1 試作。Contradiction Check を実施し NG は最大2回リトライ。
- Actions:RAG = `knowledgeSearch``knowledgeFetch`/Skill = `skill.run({ skill, input })`(excel_formula / ic5_outline / todo_extract)/Upsert = `knowledgeUpsert(items)`- コード既定:L2 Hardened-Min(入力検証/例外分類/タイムアウト/レート制限/構造化ログ/最小テスト)。

使い方(最短お試し)

  1. SeedknowledgeSeed を実行(ID=1,2,3 が入る)。
  2. 検索→取得knowledgeSearch(q="IC-5", k=3) → 出た id[]knowledgeFetch
  3. スキル
    • excel_formula{ "range": "A1:B10" }
    • ic5_outline{ "text": "Goal: ...\nDeliverables: ..." }
    • todo_extract{ "text": "todo: seed\nstep: fetch" }
  4. 知識拡張
{
  "items": [
    { "id": "my-doc", "title": "IC-5 ガイド", "body": "..." },
    { "title": "章2", "body": "..." }
  ]
}

→ upsert 後に search→fetch して Sources に ID/要旨を列挙。


無料で作るメリット(アピール)

  • 初期費用ゼロ:Runner/Qdrant/埋め込みが無料枠で動く。作って壊して学ぶが気軽。
  • 人間らしく前に進む:曖昧な指示でも既定値で 0.1 試作→早い合意形成。
  • 根拠でぶれない:RAG と Sources により“言い切り”の暴走を抑制。
  • 運用に強い:L2 ハードニング(レート制限/タイムアウト/例外分類/構造化ログ)。
  • 拡張しやすい/skill/run に関数を足していくだけで作業の自動化を増やせる。

よく出るエラーと即復旧

  • 403(create collection failed):Qdrant 認証/URL。不明なら /debug/qdrant-auth を実行→200 が出るヘッダ形式で固定。
  • 500(embedding failed):Workers AI Binding 未設定 or モデル名違い。/debug/embed-dim で確認。必要なら CF_ACCOUNT_ID/CF_API_TOKEN を設定して REST フォールバック。
  • OpenAPI 赤線components:paths: と同階層に。インライン {} を減らし多段で書く。
  • 429(rate limit)/skill/run は 1分30回の簡易制限。負荷テスト時は待つ or 値を調整。

ランナー完全ソース

worker.js
// Golden Runner (Hardened) — Cloudflare Workers AI + Qdrant
// - 任意文字IDでもUUIDに正規化(一般Upsert/Fetch両方)
// - ベクトル次元→自動で次元付きコレクションを作成し、エイリアスへ張替え
// - 認証ヘッダは api-key / Api-Key / Bearer を自動試行
// - 埋め込みは WorkersAI 'text' / 'input' / 'input_text' を順に試し、HTTPフォールバックも可
export default {
  async fetch(req, env) {
    const url = new URL(req.url);
    const path = url.pathname;

    // CORS
    if (req.method === "OPTIONS") return new Response(null, { headers: cors() });

    try {
      // ---- Health
      if (path === "/" && req.method === "GET") {
        return json({ ok: true, service: "runner", now: new Date().toISOString() });
      }

      // ---- Debug: embed dim
      if (path === "/debug/embed-dim" && req.method === "GET") {
        const v = await embed(env, "health");
        return json({ ok: true, dim: v.length });
      }

      // ---- Debug: qdrant auth (200/ERR 一覧)
      if (path === "/debug/qdrant-auth" && req.method === "GET") {
        const base = baseUrl(env);
        const results = [];
        for (const h of qdrantHeaderVariants(env.QDRANT_API_KEY)) {
          try {
            const r = await tfetch(`${base}/collections`, { headers: h }, 8000);
            results.push({ header: Object.keys(h).find(k=>k!=="content-type"), status: r.status });
          } catch (e) {
            results.push({ header: Object.keys(h).find(k=>k!=="content-type"), status: "ERR", message: String(e?.message||e) });
          }
        }
        return json({ ok: true, results });
      }

      // ---- Seed(整数ID 1,2,3 固定:確実に通る)
      if (path === "/knowledge/seed" && req.method === "GET") {
        const items = [
          { title: "Skills/Actionsの考え方", body: "Actions=外部実行。RAG=一次資料検索。" },
          { title: "Hardened-Min",          body: "入力検証/例外/タイムアウト/レート/ログ/最小テスト。" },
          { title: "IC-5",                  body: "Goal/Deliverable/Constraints/Defaults/Non-Goals。" }
        ];
        const texts = items.map(it => `${it.title}\n${it.body}`);
        const vecs  = await embedMany(env, texts);
        await ensureCollection(env, vecs[0].length);

        const points = items.map((it, i) => ({
          id: i + 1,
          vector: vecs[i],
          payload: { ...it, seed_orig: `p${i+1}` }
        }));
        await qdrantUpsert(env, points);
        return json({ ok: true, upserted: points.map(p=>p.id) });
      }

      // ---- Search
      if (path === "/knowledge/search" && req.method === "GET") {
        const q = url.searchParams.get("q") || "";
        const k = clampInt(url.searchParams.get("k") || "5", 1, 20);
        if (!q) return json({ ok:false, error:"q required" }, 400);

        const vec = await embed(env, q);
        await ensureCollection(env, vec.length);
        const hits = await qdrantSearch(env, vec, k);
        return json({ ok:true, items: hits.map(h => ({
          id: h.id, score: h.score, title: h.payload?.title, body: h.payload?.body
        }))});
      }

      // ---- Fetch(ID正規化して取得)
      if (path === "/knowledge/fetch" && req.method === "POST") {
        const body = await safeJson(req);
        const rawIds = Array.isArray(body?.ids) ? body.ids : [];
        if (!rawIds.length) return json({ ok:false, error:"ids required" }, 400);

        const ids = await Promise.all(rawIds.map(x => normalizePointId(x)));
        const items = await qdrantRetrieve(env, ids);
        return json({ ok:true, items });
      }

      // ---- Upsert(単体/配列 OK・任意文字ID→UUID化)
      if (path === "/knowledge/upsert" && req.method === "POST") {
        const body = await safeJson(req);
        let items = body?.items;
        if (!items) return json({ ok:false, error:"items required" }, 400);
        if (!Array.isArray(items)) items = [items];

        const texts = items.map(it => `${it.title ?? ""}\n${it.body ?? ""}`);
        const vecs  = await embedMany(env, texts);
        await ensureCollection(env, vecs[0].length);

        const points = await Promise.all(items.map(async (it, i) => ({
          id: it.id ? await normalizePointId(it.id) : cryptoRandomUUID(),
          vector: vecs[i],
          payload: it
        })));

        await qdrantUpsert(env, points);
        return json({ ok:true, upserted: points.map(p=>p.id) });
      }

      // ---- /skill/run(L2: Hardened-Min)
      if (path === "/skill/run" && req.method === "POST") {
        // 入口レート制限(IP×エンドポイント、1分30回)
        const ip =
          req.headers.get("CF-Connecting-IP") ||
          req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
          "unknown";
        const rl = enforceRateLimit(env, `skill:${ip}`, 30, 60_000);
        if (!rl.ok) {
          return json({ ok: false, code: "RATE_LIMIT", retry_after_ms: rl.retryAfterMs }, 429);
        }

        // 入力検証
        const body  = await safeJson(req);
        const skill = typeof body?.skill === "string" ? body.skill.trim() : "";
        const input = (body && typeof body.input === "object") ? body.input : {};
        if (!skill) return json({ ok:false, code:"BAD_REQUEST", message:"skill required" }, 400);
        if (JSON.stringify(input).length > 40_000) {
          return json({ ok:false, code:"PAYLOAD_TOO_LARGE", limit: 40000 }, 413);
        }

        // スキル定義
        const skills = {
          excel_formula: async (inp) => {
            const range = typeof inp?.range === "string" ? inp.range : "A1:A10";
            return { formula: `=SUM(${range})` };
          },
          ic5_outline: async (inp) => {
            const text = String(inp?.text ?? "");
            const keep = (s) => (s || "").toString().slice(0, 4000);
            const pick = (re) => keep((text.match(re) || [])[1] || "");
            return {
              goal:        pick(/(?:^|\b)goal[::]\s*(.+)/i),
              deliverable: pick(/(?:^|\b)deliverable[s]?[::]\s*(.+)/i),
              constraints: pick(/(?:^|\b)constraint[s]?[::]\s*(.+)/i),
              defaults:    pick(/(?:^|\b)default[s]?[::]\s*(.+)/i),
              nonGoals:    pick(/(?:^|\b)non[- ]?goal[s]?[::]\s*(.+)/i),
            };
          },
          todo_extract: async (inp) => {
            const text  = String(inp?.text ?? "");
            const lines = text.split(/\r?\n/).map(s=>s.trim()).filter(Boolean);
            const prefixRe = /^(?:todo|task|step|action)[::\-]\s*/i;
            const items = lines.filter(s => prefixRe.test(s))
                               .map(s => s.replace(prefixRe, ""))
                               .slice(0, 50);
            return { items };
          },
        };

        if (!Object.prototype.hasOwnProperty.call(skills, skill)) {
          return json({ ok:false, code:"SKILL_NOT_FOUND", message:`unknown skill: ${skill}` }, 404);
        }

        // 実行(タイムアウト/例外分類/構造化ログ)
        const TIMEOUT_MS = 5_000;
        const exec  = skills[skill];
        const timer = new Promise((_, rej) => setTimeout(() => rej(new Error("TIMEOUT")), TIMEOUT_MS));
        try {
          const output = await Promise.race([exec(input), timer]);
          console.log(JSON.stringify({ at:"/skill/run", ip, skill, ok:true }));
          return json({ ok:true, skill, output });
        } catch (e) {
          const msg  = String(e?.message || e);
          const code = msg.includes("TIMEOUT") ? "TIMEOUT"
                     : msg.includes("RATE")    ? "RATE_LIMIT"
                     : "INTERNAL";
          console.log(JSON.stringify({ at:"/skill/run", ip, skill, ok:false, code, msg }));
          return json({ ok:false, code, message: msg }, code === "TIMEOUT" ? 504 : 500);
        }
      }

      // ここまで来たら未定義ルート
      return json({ ok:false, error: "Not Found" }, 404);

    } catch (e) {
      return json({ ok:false, code:"INTERNAL", message: String(e?.message || e) }, 500);
    }
  }
};

// ===== Helpers =====
function cors(){ return {
  "Access-Control-Allow-Origin":"*",
  "Access-Control-Allow-Methods":"GET,POST,OPTIONS",
  "Access-Control-Allow-Headers":"Content-Type,Authorization"
};}
function json(obj,status=200){
  return new Response(JSON.stringify(obj),{ status, headers:{ "content-type":"application/json", ...cors() }});
}
async function safeJson(req){ try{ return await req.json(); } catch{ return {}; } }
function clampInt(v,min,max){ const n=parseInt(String(v),10); return Math.max(min,Math.min(max,isNaN(n)?min:n)); }

// UUID v4(Workers 標準API利用、未実装環境でも自前生成)
function cryptoRandomUUID() {
  if (crypto.randomUUID) return crypto.randomUUID();
  const b = new Uint8Array(16);
  crypto.getRandomValues(b);
  b[6] = (b[6] & 0x0f) | 0x40; // version 4
  b[8] = (b[8] & 0x3f) | 0x80; // variant
  const hex = [...b].map(x => x.toString(16).padStart(2, "0")).join("");
  return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`;
}

// BEST-EFFORT レート制限(単一インスタンス)
// 厳密制御が必要なら DO/KV or WAF を併用
function enforceRateLimit(env, bucket, max, windowMs = 60_000) {
  env.__RL ||= new Map();
  const now = Date.now();
  let s = env.__RL.get(bucket);
  if (!s || now - s.start >= windowMs) s = { start: now, count: 0 };
  s.count++;
  env.__RL.set(bucket, s);
  if (s.count > max) {
    return { ok:false, retryAfterMs: Math.max(0, windowMs - (now - s.start)) };
  }
  return { ok:true };
}

async function tfetch(url,init,ms=15000){
  const c=new AbortController(); const id=setTimeout(()=>c.abort("timeout"),ms);
  try{ return await fetch(url,{...init,signal:c.signal}); } finally{ clearTimeout(id); }
}

// ---- Embedding (robust: binding→HTTP→optional fake)
function modelList(env){
  const m=[]; if (env.EMBED_MODEL) m.push(env.EMBED_MODEL);
  m.push("@cf/baai/bge-base-ja-v1"); // JP安定
  m.push("@cf/baai/bge-m3");         // 多言語
  return m;
}
async function embed(env, text){
  for (const m of modelList(env)){
    const tries = [
      () => env.AI?.run?.(m, { text:[text] }),
      () => env.AI?.run?.(m, { input:[text] }),
      () => env.AI?.run?.(m, { input_text:[text] }),
      () => aiHttp(env, m, { text:[text] }),
      () => aiHttp(env, m, { input:[text] }),
      () => aiHttp(env, m, { input_text:[text] })
    ];
    for (const t of tries){
      try{
        const out = await t();
        const v = out?.data?.[0]?.embedding ?? out?.result?.data?.[0]?.embedding ?? out?.data?.[0];
        if (Array.isArray(v)) return v;
      }catch{}
    }
  }
  if (String(env.ALLOW_FAKE_EMBED)==="1") return fakeEmbed(text, 768);
  throw new Error("embedding failed");
}
async function embedMany(env, texts){
  for (const m of modelList(env)){
    const tries = [
      () => env.AI?.run?.(m, { text:texts }),
      () => env.AI?.run?.(m, { input:texts }),
      () => env.AI?.run?.(m, { input_text:texts }),
      () => aiHttp(env, m, { text:texts }),
      () => aiHttp(env, m, { input:texts }),
      () => aiHttp(env, m, { input_text:texts })
    ];
    for (const t of tries){
      try{
        const out = await t();
        const arr = out?.data?.map(d=>d.embedding) ?? out?.result?.data?.map(d=>d.embedding) ?? (Array.isArray(out?.data?.[0])?out.data:null);
        if (Array.isArray(arr) && Array.isArray(arr[0])) return arr;
      }catch{}
    }
  }
  const seq=[]; for (const tx of texts) seq.push(await embed(env, tx)); return seq;
}
async function aiHttp(env, m, payload){
  if (!env.CF_ACCOUNT_ID || !env.CF_API_TOKEN) throw new Error("http not configured");
  const url=`https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/ai/run/${m}`;
  const r=await tfetch(url,{ method:"POST", headers:{ "Authorization":`Bearer ${env.CF_API_TOKEN}`, "content-type":"application/json" }, body: JSON.stringify(payload) }, 20000);
  const j=await r.json(); return j?.result ?? j;
}
function fakeEmbed(text, dim){
  const out=new Array(dim); let h=2166136261;
  for (let i=0;i<text.length;i++){ h^=text.charCodeAt(i); h=Math.imul(h,16777619); }
  for (let i=0;i<dim;i++){ h^=(h>>>13); h=Math.imul(h,1274126177); out[i]=(h%2000)/1000-1; }
  return out;
}

// ---- Qdrant (alias + header variants)
function baseUrl(env){ return String(env.QDRANT_URL||"").replace(/\/+$/,""); }
function qdrantHeaderVariants(key){
  return [
    { "content-type":"application/json", "api-key": key },
    { "content-type":"application/json", "Authorization": `Api-Key ${key}` },
    { "content-type":"application/json", "Authorization": `Bearer ${key}` }
  ];
}
async function qfetch(env, endpoint, init={}){
  const base=baseUrl(env); const key=env.QDRANT_API_KEY; let last="";
  for (const h of qdrantHeaderVariants(key)){
    try{
      const r=await tfetch(`${base}${endpoint}`, { ...init, headers:{ ...(init.headers||{}), ...h } }, 15000);
      if (r.status!==401 && r.status!==403) return r;
      last=await r.text().catch(()=>`status=${r.status}`);
    }catch(e){ last=String(e?.message||e); }
  }
  throw new Error(`qdrant auth failed: ${last}`);
}
async function ensureCollection(env, size){
  const alias = env.QDRANT_COLLECTION;     // 常用名
  const real  = `${alias}_d${size}`;       // 実体は次元付き
  let head = await qfetch(env, `/collections/${real}`, { method:"GET" });
  if (!head.ok){
    const created = await qfetch(env, `/collections/${real}`, {
      method:"PUT", body: JSON.stringify({ vectors:{ size, distance:"Cosine" } })
    });
    if (!created.ok) throw new Error(`create collection failed: ${await created.text()}`);
  }
  await qfetch(env, `/collections/aliases`, { method:"POST", body: JSON.stringify({ actions:[{ delete_alias:{ alias_name: alias } }] }) }).catch(()=>{});
  await qfetch(env, `/collections/aliases`, { method:"POST", body: JSON.stringify({ actions:[{ create_alias:{ collection_name: real, alias_name: alias } }] }) });
  return true;
}
async function qdrantUpsert(env, points){
  const r=await qfetch(env, `/collections/${env.QDRANT_COLLECTION}/points`, {
    method:"PUT", body: JSON.stringify({ points })
  });
  if (!r.ok) throw new Error(`upsert failed: ${await r.text()}`);
}
async function qdrantSearch(env, vector, k){
  const r=await qfetch(env, `/collections/${env.QDRANT_COLLECTION}/points/search`, {
    method:"POST", body: JSON.stringify({ vector, limit:k, with_payload:true })
  });
  if (!r.ok) throw new Error(`search failed: ${await r.text()}`);
  const j=await r.json(); return j?.result ?? [];
}
async function qdrantRetrieve(env, ids){
  const r=await qfetch(env, `/collections/${env.QDRANT_COLLECTION}/points`, {
    method:"POST", body: JSON.stringify({ ids, with_payload:true })
  });
  if (!r.ok) throw new Error(`retrieve failed: ${await r.text()}`);
  const j=await r.json(); return j?.result ?? [];
}

// ---- PointId 正規化(整数/UUID へ統一)
const UUID_RE=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
async function normalizePointId(raw){
  if (raw===undefined||raw===null) return cryptoRandomUUID();
  if (typeof raw==="number") return raw;
  if (typeof raw==="string"){
    const s=raw.trim();
    if (/^\d+$/.test(s)) return parseInt(s,10);
    if (UUID_RE.test(s)) return s.toLowerCase();
    // 任意文字列→決定的 UUID(SHA-256 の先頭16Bを v4 に整形)
    const enc=new TextEncoder().encode(s);
    const buf=await crypto.subtle.digest("SHA-256",enc);
    const b=new Uint8Array(buf).slice(0,16);
    b[6]=(b[6]&0x0f)|0x40; b[8]=(b[8]&0x3f)|0x80;
    const hex=[...b].map(x=>x.toString(16).padStart(2,"0")).join("");
    return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`;
  }
  return cryptoRandomUUID();
}

まとめ

  • “人の指示の曖昧さ”を補完しつつ、根拠でぶれない Custom GPT を、クラウド費 0円で構築できました。
  • あとは skill.run に小さな関数を足していけば、日々の作業を少しずつ自動化できます。
  • Thinking モデルが使えない場合でも、Clarify-or-Assume + 0.1試作で“人間らしい”挙動を再現できます。
  • フィードバックも歓迎していますので、より良く出来る機能・改善点など教えていただけると嬉しいです。 
    --
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?