هوش مصنوعی در اکسل و گوگل شیت؛ ساخت تابع AI با وبسرویس
یک شیت دارید با پانصد عنوان کالا در ستون A. ستون B باید برای هرکدام یک توضیح بیست کلمهای فارسی بگیرد. کپی کردن هر سطر در مرورگر و برگرداندن جواب، دو روز کامل کار دستی است و روز دوم لحن متنها دیگر شبیه روز اول نیست. راه درست این است که خود صفحهگسترده با وبسرویس (API) حرف بزند و ستون B را خودش پر کند.
در این راهنما اول یک تابع سفارشی در گوگل شیت میسازیم، بعد همان کار را در اکسل با Power Query و با ماکرو انجام میدهیم، و در پایان به مهمترین قسمت میرسیم: چطور پانصد سطر را با بیستوپنج درخواست پر کنیم، نه با پانصد درخواست.
پیش از شروع، یک انتظار را تنظیم کنیم. صفحهگسترده برای کارهایی خوب است که ورودی و خروجی هر سطر کوتاه و مستقل باشد: دستهبندی، یکدست کردن نامها، ترجمهٔ عنوان، بیرون کشیدن یک عدد از متن، یا نوشتن یک توضیح کوتاه. هر جا سطرها به هم وابستهاند یا خروجی باید چند پاراگراف باشد، همان اول کار را به یک اسکریپت بسپارید؛ شیت در آن حالت هم کند میشود و هم گران.
گوگل شیت: یک تابع سفارشی در ده دقیقه
از منوی «Extensions» وارد «Apps Script» شوید. پیش از هر کدی، کلید را در جای درستش بگذارید: در «Project Settings» بخش «Script properties» یک ردیف با نام NETARZ_API_KEY بسازید و مقدار کلید را همانجا ذخیره کنید. کلید هرگز نباید داخل خود شیت یا داخل کد بنشیند، چون هر کسی که شیت را ببیند آن را هم میبیند.
/**
* تابع سفارشی صفحهگسترده. نمونهٔ استفاده در سلول:
* =AI("خلاصه کن", A2)
* =AI("یک توضیح ۲۰ کلمهای بنویس", A2, "gpt-4o-mini")
*/
function AI(instruction, input, model) {
var key = PropertiesService.getScriptProperties().getProperty('NETARZ_API_KEY');
if (!key) throw new Error('کلید در Script properties ذخیره نشده است');
if (!input || String(input).trim() === '') return '';
var payload = {
model: model || 'gpt-4o-mini',
max_tokens: 200,
temperature: 0.2,
messages: [
{ role: 'system', content: 'You are a spreadsheet cell. Reply in Persian, one short line, no preamble, no quotes.' },
{ role: 'user', content: instruction + '\n\n' + String(input) }
]
};
var res = UrlFetchApp.fetch('https://netarz.ir/api/ai/v1/chat/completions', {
method: 'post',
contentType: 'application/json',
headers: { Authorization: 'Bearer ' + key },
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
var body = JSON.parse(res.getContentText());
if (res.getResponseCode() !== 200) {
return 'خطا: ' + ((body.error && body.error.code) || res.getResponseCode());
}
return String(body.choices[0].message.content).trim();
}
ذخیره کنید، به شیت برگردید و در سلول B2 فرمول را بنویسید. دو نکتهٔ کوچک: در تنظیمات فارسی، جداکنندهٔ آرگومانها ممکن است بهجای ویرگول، نقطهویرگول باشد؛ و پارامتر سوم اختیاری است تا برای سطرهای سختتر مدل بزرگتری بگذارید بدون اینکه کد عوض شود.
دو جای این کد بیشترین اثر را روی کیفیت خروجی دارند. دستور سیستم به مدل میگوید خروجی باید یک خط کوتاه، بدون مقدمه و بدون گیومه باشد؛ بدون آن جمله، مدل معمولاً با یک تعارف شروع میکند و سلول شما پر از حرف اضافه میشود. مقدار پایین temperature هم یعنی سطرهای مشابه خروجی مشابه بگیرند، که در یک ستون دقیقاً همان چیزی است که میخواهید.
تلهٔ اصلی: پانصد سلول یعنی پانصد درخواست
تابع سفارشی زیباست، اما سه رفتار دارد که اگر ندانید، صورتحساب را غافلگیرکننده میکند:
- هر سلول یک درخواست جداست. پانصد سلول یعنی پانصد بار ارسال دستور سیستم و پانصد بار سربار شبکه.
- فرمولها دوباره حساب میشوند. باز کردن دوبارهٔ فایل، ویرایش یک سلول بالادستی یا کپی کردن شیت میتواند همه را از نو اجرا کند. نتیجهای که فکر میکردید یک بار خریدهاید، بار دوم و سوم هم هزینه دارد.
- سقف زمان اجرا وجود دارد. هر تابع سفارشی حدود سی ثانیه فرصت دارد؛ پاسخهای طولانی یا مدلهای کند، سلول را با پیام «Exceeded maximum execution time» خراب میکنند.
قاعدهای که خودمان رعایت میکنیم: تابع سفارشی برای چند ده سلول و کار اکتشافی خوب است. برای ستونهای بزرگ، سراغ یک دکمه در منو بروید که نتیجه را بهصورت مقدار مینویسد، نه فرمول. اگر هم با فرمول جلو رفتید، پس از تمام شدن کار ستون را کپی و با «Paste values only» روی خودش جایگذاری کنید تا از آن به بعد دیگر چیزی حساب نشود.
نسخهٔ دستهای: بیست سطر در یک درخواست
اینجا جایی است که هزینه واقعاً پایین میآید. بهجای یک سطر در هر درخواست، بیست سطر را با شماره میفرستیم و یک آرایهٔ JSON با همان ترتیب میگیریم:
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('هوش مصنوعی')
.addItem('پر کردن ستون B از روی A', 'fillColumnB')
.addToUi();
}
function fillColumnB() {
var sheet = SpreadsheetApp.getActiveSheet();
var last = sheet.getLastRow();
var rows = sheet.getRange(2, 1, last - 1, 1).getValues().map(function (r) { return String(r[0]).trim(); });
var out = [];
var CHUNK = 20;
for (var i = 0; i < rows.length; i += CHUNK) {
out = out.concat(askBatch(rows.slice(i, i + CHUNK)));
Utilities.sleep(400); // فاصله، تا به سقف درخواست در دقیقه نخورید
sheet.getRange(2, 2, out.length, 1).setValues(out.map(function (v) { return [v]; }));
}
}
function askBatch(items) {
var key = PropertiesService.getScriptProperties().getProperty('NETARZ_API_KEY');
var numbered = items.map(function (t, i) { return (i + 1) + '. ' + t; }).join('\n');
var payload = {
model: 'gpt-4o-mini',
max_tokens: 1200,
response_format: { type: 'json_object' },
messages: [
{ role: 'system', content: 'Return JSON: {"items": ["…", "…"]} with exactly one Persian string per numbered input, same order.' },
{ role: 'user', content: 'برای هر عنوان یک توضیح ۲۰ کلمهای فارسی بنویس:\n' + numbered }
]
};
var res = UrlFetchApp.fetch('https://netarz.ir/api/ai/v1/chat/completions', {
method: 'post', contentType: 'application/json',
headers: { Authorization: 'Bearer ' + key },
payload: JSON.stringify(payload), muteHttpExceptions: true
});
var body = JSON.parse(res.getContentText());
if (res.getResponseCode() !== 200) throw new Error(body.error ? body.error.message : 'HTTP ' + res.getResponseCode());
var parsed = JSON.parse(body.choices[0].message.content);
var list = parsed.items || [];
while (list.length < items.length) list.push(''); // اگر مدل کم برگرداند، ترتیب به هم نریزد
return list.slice(0, items.length);
}
سه محافظ کوچک در این کد عمداً هست و هر سه از تجربه آمدهاند: نوشتن نتیجه بعد از هر دسته (تا قطع شدن کار وسط راه، کار انجامشده را از بین نبرد)، فاصلهٔ کوتاه بین دستهها، و همطول کردن خروجی با ورودی. نکتهٔ قالب JSON را هم مفصلتر در خروجی JSON قابل اعتماد از مدل نوشتهایم.
اندازهٔ دسته را هم چشمبسته بزرگ نکنید. هرچه دسته بزرگتر شود سربار کمتر میشود، ولی احتمال اینکه مدل یک سطر را جا بیندازد یا ترتیب را به هم بزند بالا میرود و سقف خروجی هم زودتر پر میشود. بین ده تا بیست سطر برای بیشتر کارهای کوتاه نقطهٔ تعادل خوبی است؛ اگر خروجی هر سطر بلند است، دسته را کوچکتر بگیرید و max_tokens را متناسب با آن بالا ببرید.
اکسل، راه اول: Power Query
در اکسل مسیر بدون افزونه همین است. از «Data» به «Get Data» و «Launch Power Query Editor» بروید و یک کوئری خالی بسازید:
let
Key = "sk-ntz-v1-...",
Ask = (prompt as text) as text =>
let
Body = Json.FromValue([
model = "gpt-4o-mini",
max_tokens = 200,
messages = {[role = "user", content = prompt]}
]),
Raw = Web.Contents(
"https://netarz.ir/api/ai/v1",
[
RelativePath = "chat/completions",
Headers = [#"Content-Type" = "application/json", Authorization = "Bearer " & Key],
Content = Body
]
),
Parsed = Json.Document(Raw),
Answer = Parsed[choices]{0}[message][content]
in
Answer,
Source = Excel.CurrentWorkbook(){[Name = "Titles"]}[Content],
Result = Table.AddColumn(Source, "description", each Ask("یک توضیح ۲۰ کلمهای فارسی برای: " & [title]))
in
Result
دو نکتهٔ عملی. اول اینکه جدول ورودی باید یک «Table» با نام مشخص باشد (اینجا Titles)، وگرنه Excel.CurrentWorkbook آن را پیدا نمیکند. دوم اینکه اکسل بار اول برای نشانی تازه سراغ «Data source settings» میرود؛ دسترسی را روی Anonymous بگذارید، چون احراز هویت را خودتان در سربرگ فرستادهاید.
یک هشدار صادقانه هم لازم است: کلید داخل خود فایل اکسل ذخیره میشود، پس هر کسی که فایل را بگیرد کلید را هم دارد. برای این کار یک کلید جداگانه با سقف هزینهٔ کوچک بسازید و اگر فایل دست به دست شد، همان کلید را باطل کنید.
اکسل، راه دوم: ماکرو VBA
اگر کاربران فایل با Power Query راحت نیستند، یک تابع VBA همان تجربهٔ سلول را میدهد:
Function AI(instruction As String, cellText As String) As String
Dim http As Object, payload As String, raw As String, p1 As Long, p2 As Long
If Len(Trim$(cellText)) = 0 Then AI = "": Exit Function
payload = "{""model"":""gpt-4o-mini"",""max_tokens"":200,""messages"":[{""role"":""user""," & _
"""content"":""" & JsonEscape(instruction & " :: " & cellText) & """}]}"
Set http = CreateObject("MSXML2.ServerXMLHTTP.6.0")
http.Open "POST", "https://netarz.ir/api/ai/v1/chat/completions", False
http.setRequestHeader "Content-Type", "application/json"
http.setRequestHeader "Authorization", "Bearer " & Environ("NETARZ_API_KEY")
http.send payload
raw = http.responseText
If http.Status <> 200 Then AI = "خطا: " & http.Status: Exit Function
p1 = InStr(raw, """content"":""") + 11
p2 = InStr(p1, raw, """")
AI = Replace(Mid$(raw, p1, p2 - p1), "\n", vbLf)
End Function
Private Function JsonEscape(s As String) As String
JsonEscape = Replace(Replace(Replace(s, "\", "\\"), """", "\"""), vbLf, "\n")
End Function
این تابع عمداً ساده نگه داشته شده و پاسخ را با جستوجوی متنی بیرون میکشد؛ برای متنهای کوتاه کافی است، ولی اگر خروجی گیومه یا نویسهٔ خاص داشت، یک کتابخانهٔ JSON واقعی به فایل اضافه کنید. کلید را هم در متغیر محیطی ویندوز بگذارید تا داخل فایل نماند.
اگر فایل اکسل قرار است بین چند نفر بچرخد، بهجای ماکرو یک سرویس کوچک روی سرور خودتان بگذارید و ماکرو را به آن وصل کنید. آن وقت کلید فقط یک جا میماند، سقف مصرف هر کاربر را خودتان تعیین میکنید، و بستن دسترسی یک نفر بقیه را از کار نمیاندازد.
هزینه: همان کار، دو صورتحساب متفاوت
فرض کنید پانصد سطر دارید، دستور سیستم حدود ۶۰ توکن است، هر عنوان حدود ۲۰ توکن و هر توضیح حدود ۶۰ توکن خروجی. تفاوت دو روش این میشود:
| روش | تعداد درخواست | توکن ورودی | توکن خروجی | ریسک |
|---|---|---|---|---|
| هر سلول یک فرمول | ۵۰۰ | حدود ۴۰٬۰۰۰ | حدود ۳۰٬۰۰۰ | اجرای دوباره در هر بازکردن فایل |
| دستههای بیستتایی | ۲۵ | حدود ۱۳٬۵۰۰ | حدود ۳۰٬۰۰۰ | ترتیب خروجی باید کنترل شود |
توکن خروجی تقریباً ثابت میماند، چون همان تعداد توضیح نوشته میشود؛ صرفهجویی در ورودی و در سربار است. برای متن فارسی این عددها را دستبالا بگیرید، چون هر کلمهٔ فارسی توکن بیشتری از انگلیسی میگیرد. روش شمردن دقیقتر در توکن چیست و هزینه چطور حساب میشود آمده و الگوهای مهار خرج در کنترل هزینهٔ API هوش مصنوعی.
نکتهٔ دیگری هم هست که در جدول دیده نمیشود: هر درخواست جدا یک بار در سقف درخواست در دقیقه شمرده میشود. پانصد فرمول که همزمان اجرا شوند بهسرعت به آن سقف میخورند و بخشی از سلولها با خطای ۴۲۹ پر میشوند؛ بعد باید دستی بگردید ببینید کدام سطرها ناقص ماندهاند. روش دستهای این دردسر را هم از بین میبرد.
یک جمعبندی کوتاه از تجربه: اگر ستون شما زیر پنجاه سطر است با فرمول جلو بروید، بین پنجاه تا چند هزار سطر با دکمهٔ دستهای، و بالاتر از آن کار را از صفحهگسترده بیرون ببرید و در یک اسکریپت با صف اجرا کنید.
وقتی سلول بهجای جواب، خطا نشان میدهد
- «خطا: invalid_api_key» — کلید در Script properties ذخیره نشده یا نام خاصیت با کد یکی نیست.
- «خطا: 429» — چند ده فرمول همزمان اجرا شدهاند. دستهای کار کنید و بین دستهها فاصله بگذارید.
- «خطا: 402» — موجودی اعتبار برای رزرو این درخواست کافی نیست؛ شارژ کنید یا
max_tokensرا کمتر بگیرید. - «Exceeded maximum execution time» — پاسخ از سی ثانیه طولانیتر شده. سقف خروجی را پایین بیاورید یا مدل سبکتری بگذارید.
- سلول خالی میماند — ورودی خالی بوده، که همان رفتار درست کد بالاست.
قدم بعدی
اگر این مسیر را تا انتها رفتید و جواب گرفت، قدم بعدی معمولاً بیرون آوردن کار از صفحهگسترده است: همان منطق دستهای در یک اسکریپت یا در پنل فروشگاه مینشیند و نتیجه مستقیم در دیتابیس ذخیره میشود. نمونهٔ ووکامرسی همین کار را در تولید انبوه توضیحات محصول نوشتهایم.
برای شروع، یک کلید با سقف هزینهٔ کوچک از پنل بسازید و اول روی ده سطر امتحان کنید؛ خروجی ده سطر میگوید دستور سیستم شما چقدر دقیق است. مفاهیم پایه در وبسرویس هوش مصنوعی چیست و فهرست مدلهای فعال با نرخشان در صفحهٔ وبسرویس هوش مصنوعی آمده است.
پرسشهای پرتکرار
کلید API را کجای گوگل شیت بگذارم که لو نرود؟
در Apps Script و در بخش Script properties، نه داخل سلول و نه داخل کد. هر کسی که شیت را ببیند سلولها را میبیند، ولی خاصیتهای اسکریپت با فایل به اشتراک گذاشته نمیشوند. برای اکسل که کلید داخل فایل میماند، یک کلید جداگانه با سقف هزینهٔ کوچک بسازید.
چرا فرمولهای AI دوباره اجرا میشوند و هزینه میگیرند؟
چون تابع سفارشی مثل هر فرمول دیگری با باز شدن فایل یا تغییر سلولهای وابسته از نو حساب میشود. بعد از پر شدن ستون، آن را کپی کنید و با گزینهٔ «Paste values only» روی خودش جایگذاری کنید تا نتیجه به مقدار ثابت تبدیل شود.
برای ۵۰۰ سطر بهتر است فرمول بنویسم یا دکمهٔ منو بسازم؟
دکمهٔ منو. فرمول برای چند ده سلول و کار اکتشافی مناسب است، اما در ستونهای بزرگ هم گرانتر تمام میشود و هم به سقف زمان اجرا میخورد. تابع دستهای که بیست سطر را در یک درخواست میفرستد، تعداد درخواست را بیست برابر کم میکند.
خروجی در سلولها جابهجا میشود؛ چه کار کنم؟
از مدل بخواهید خروجی را به شکل JSON با یک آرایه و همان ترتیب ورودی برگرداند و پیش از نوشتن، طول آرایه را با تعداد سطرهای ارسالی برابر کنید. اگر مدل کمتر برگرداند، جاهای خالی را با رشتهٔ خالی پر کنید تا ترتیب ستون به هم نریزد.
نظر خوانندگان
هنوز نظری ثبت نشده. اگر این مقاله پرسشتان را جواب داد یا جای چیزی در آن خالی ماند، همینجا بنویسید.