ابنِ وكيل بحث مع البحث + استخلاص الروابط (Claude tool use، من البداية للنهاية)

معظم دروس 'وكيل البحث بالذكاء الاصطناعي' تتوقّف عند 'هذا تعريف أداة'. هذا الدرس يشحن وكيلًا عاملًا: سؤال يدخل، إجابة مع استشهادات تخرج. بحث، استخلاص، استدلال، استشهاد — كل ذلك في أقل من 120 سطرًا من Python.
الخلاصة
- •أصغر حلقة وكيل بحث مفيدة هي: بحث ← اختيار روابط ← استخلاص النصوص ← مطالبة النموذج بالإجابة مع استشهادات داخلية.
- •أداتان — Web Search (15 رصيدًا) وURL Extract (رصيدان لكل رابط) — تغطّيان 95% من حالات الإجابة المؤسَّسة على مصادر.
- •Claude tool use يتولّى التنسيق؛ وأنت فقط تنقل كتل tool_use ← نداءات API ← كتل tool_result حتى يتوقّف النموذج.
- •التكلفة من البداية للنهاية: نحو 25 رصيدًا + نحو $0.02 من رموز نموذج اللغة لكل سؤال بعمقٍ نموذجي.
ما الذي نبنيه
وكيل بحث يأخذ مُدخلًا واحدًا — سؤالًا بلغة طبيعية — ويُرجِع إجابةً مع مصادر. معماريًا:
question → [Claude]
↓ tool_use(search)
[API Pick Web Search] → ranked URLs
↓ tool_use(extract)
[API Pick URL Extract] → cleaned bodies
↓ Claude reads, decides if more is needed
↓ end_turn
answer with inline [source: URL] citationsنحو 120 سطرًا من Python. أداتان — البحث والاستخلاص — وحلقة وكيل واحدة تتعامل مع أيّ شيء يقرّره Claude. سنبنيه من الصفر في 4 خطوات.
1اسحب مخطّطات الأدوات (دون كتابة JSON يدويًا)
كلتا نقطتي النهاية تنشر مسار tool-schema يُرجِع تعريف أداة Claude بالشكل الذي يتوقّعه messages.create بالضبط.
import requests
API_KEY = "pk_yourkey"
def fetch_tool(tool_path: str) -> dict:
"""Fetch a Claude tool definition from API Pick's tool-schema endpoint."""
schema = requests.get(f"https://www.apipick.com{tool_path}/tool-schema").json()
return schema["claude"]
WEB_SEARCH_TOOL = fetch_tool("/api/search/web")
URL_EXTRACT_TOOL = fetch_tool("/api/extract")
# Cache these once at module load. They don't change between requests.أمران مهمّان يمنحك إيّاهما هذا مجانًا: مخطّط المعاملات (ليعرف Claude أنك تقبل query وcountry_code وstart_date إلخ) ووصفٌ واضح وملائم للنموذج لما تفعله الأداة.
2اكتب مُعالِج الأداة
عندما يُرجِع Claude كتلة tool_use، مهمّتك هي استدعاء الـ API الفعلي وإرجاع كتلة tool_result. دالة لكل أداة، ومُوزِّع واحد يوجّه بناءً على الاسم الذي اختاره Claude.
def call_tool(tool_use_block) -> dict:
"""Execute the tool Claude asked for and return a tool_result block."""
name = tool_use_block.name
args = tool_use_block.input
if name == "web_search":
endpoint = "https://www.apipick.com/api/search/web"
elif name == "extract_urls":
endpoint = "https://www.apipick.com/api/extract"
else:
return {
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": f"Unknown tool: {name}",
"is_error": True,
}
resp = requests.post(
endpoint,
headers={"x-api-key": API_KEY},
json=args,
timeout=30,
)
if resp.status_code != 200:
return {
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": f"HTTP {resp.status_code}: {resp.text[:500]}",
"is_error": True,
}
return {
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": resp.text,
}ثلاثة أمور تستحقّ الانتباه. tool_use_id هو كيف يربط Claude النتيجة بندائه السابق — يجب أن تردّه إليه. وis_error: True يخبر Claude بأن يتعافى بسلاسة (غالبًا بتجربة استعلام مختلف). ونحن نمرّر نص الاستجابة الخام — Claude مرتاح مع شكل الـ JSON الذي يُرجِعه API Pick، فلا حاجة لأي تحويل.
3حلقة الوكيل
الحلقة قصيرة: أرسل المحادثة، انظر في كل كتلة في الاستجابة، شغّل أيّ أدوات، ألحِق كلًّا من tool_use من Claude وtool_result من عندك بالمحادثة، وكرّر حتى يصبح stop_reason هو end_turn.
import anthropic
client = anthropic.Anthropic()
SYSTEM_PROMPT = """You are a research assistant. When the user asks a question:
1. Use web_search to find relevant sources. Prefer recent (set start_date) for time-sensitive queries.
2. Use extract_urls to read the body of the top 3-5 most relevant URLs from the search results.
3. Answer the question concisely. After every factual claim, include an inline citation in the form [source: URL].
4. If you don't have enough information, say so — don't fabricate.
Be concise. Aim for 3-5 sentence answers unless the user asks for depth."""
def research(question: str) -> str:
messages = [{"role": "user", "content": question}]
while True:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
system=SYSTEM_PROMPT,
tools=[WEB_SEARCH_TOOL, URL_EXTRACT_TOOL],
messages=messages,
)
# Append the assistant's response (may contain tool_use blocks)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "end_turn":
# Pull out the text answer
return "\n".join(b.text for b in response.content if b.type == "text")
if response.stop_reason == "tool_use":
# Run every tool the model asked for in this turn
tool_results = [
call_tool(b)
for b in response.content if b.type == "tool_use"
]
messages.append({"role": "user", "content": tool_results})
continue
raise RuntimeError(f"Unexpected stop_reason: {response.stop_reason}")لاحِظ أن النموذج قد يُطلِق عدّة نداءات أدوات في دور واحد — مثلًا قد يُصدِر نداء extract_urls واحدًا بخمسة روابط ليجمعها دفعةً واحدة. نتعامل مع ذلك بجمع كل كتل tool_use من دور المساعد وإرجاع كل كتل tool_result في دور المستخدم التالي.
4شغّله
if __name__ == "__main__":
answer = research("What were the major announcements at OpenAI DevDay this year?")
print(answer)سترى شيئًا كهذا:
OpenAI's DevDay focused on three themes: cheaper inference (a new gpt-mini
tier at roughly half the prior cost) [source: https://openai.com/devday-2026],
agent infrastructure (a managed agent runtime with persistent memory and
tool sandboxing) [source: https://techcrunch.com/...], and developer
tooling (a new Responses API replacing the legacy Assistants API)
[source: https://platform.openai.com/docs/...].هذا هو وكيل البحث. نحو 120 سطرًا، أداتان، وإخراج مع مصادر.
كم يكلّف هذا
نداء بحث نموذجي:
- نداء بحث واحد — 15 رصيدًا ($0.015)
- نداء استخلاص واحد يغطّي 3–5 روابط — 6–10 أرصدة ($0.006–$0.010)
- نحو 3,000 رمز إدخال + 800 رمز إخراج من Claude — نحو $0.02
بالتقريب: نحو 3 سنتات لكل إجابة مبحوثة. عند 1,000 نداء بحث يوميًا يصبح ذلك نحو $30/يوم — قريب مما تغطّيه ميزانية قهوة محلّل واحد صباحًا. ضع نموذج اللغة في الميزانية، لا واجهات البحث.
ثلاثة تحسينات للإنتاج
1. خزّن مؤقّتًا حسب السؤال
إن كان منتجك يطرح أسئلة متشابهة، فاحسب hash لـ (question, today's date) وخزّن الإجابة النهائية لمدة ساعة. معظم البحث المدفوع بالمستخدمين له معدّلات إصابة في الذيل الطويل تفوق 30%.
2. قيّد حين تهمّ الحداثة
لأسئلة 'هذا الأسبوع' / 'اليوم'، اضبط start_date في نداء البحث على التاريخ الحالي أو أمس. بدونه قد يُرجِع البحث مقالات من العام الماضي. أبسط نسخة: قاعدة في مطالبة النظام مثل 'دائمًا أدرِج start_date عندما يحتوي السؤال على اليوم، أو هذا الأسبوع، أو حديث، أو أحدث'.
3. حواجز ضدّ الهلوسة
القاعدة الأعلى أثرًا منفردةً: 'إن لم يكن لديك محتوى مُستخلَص كافٍ للإجابة، فأرجِع: لم أستطع العثور على مصدر موثوق لهذا. لا تخمّن.' إضافة هذا إلى مطالبة النظام تُخفّض الإجابات الملفّقة بمرتبة من حيث المقدار في اختباراتنا.
إلى أين تأخذه بعد ذلك
ثلاثة امتدادات طبيعية:
- اجعله رأسيًا: استبدل Web Search بـ Academic Search لوكيل مراجعة أدبيات، أو SEC Filings Search للعناية الواجبة، أو Clinical Search للبحث الطبي. المُعالِج لا يتغيّر — تتغيّر مطالبة النظام فقط.
- أضِف إخراجًا منظَّمًا: اطلب من النموذج إرجاع JSON بحقلَي
answerوcitations[]لتتمكّن من عرضهما كبطاقة واجهة. - دفّق الرموز: انتقل إلى
client.messages.stream()للإخراج التدريجي — مفيد عندما تكون الإجابة طويلة. حلقة نداء الأداة هي نفسها.
الأسئلة الشائعة
لماذا أداتان بدل نقطة نهاية واحدة لـ 'الإجابة'؟
فصل البحث عن الاستخلاص يمنحك تحكّمًا في عدد الروابط التي تقرؤها، ومتى تتوقّف، وأيّها تستشهد به. نقطة نهاية 'إجابة' مُستضافة واحدة تخفي هذا — لا يمكنك بسهولة تغيير الاستراتيجية دون إعادة هندسة. مع أداتين، يصبح الكود نفسه وكيل إيجاز صباحي، أو مُراجِع أدبيات، أو كاشطًا للاستخبارات التنافسية بمجرّد تعديل المطالبة (prompt).
هل يعمل هذا مع OpenAI Assistants / Responses API أيضًا؟
نعم. البنية متطابقة — الشيء الوحيد الذي يتغيّر هو كيفية تحليلك لكتل نداء الأداة وتقديم النتائج. يصبح شكل المُعالِج submit_tool_outputs(...) بدلًا من كتلة محتوى tool_result، لكن حلقة الوكيل وتعريفات الأدوات هي نفس الـ JSON.
كيف يقرّر الوكيل متى يتوقّف؟
يُرجِع Claude القيمة stop_reason: end_turn عندما يملك سياقًا كافيًا للإجابة، وtool_use عندما يريد استدعاء أداة. الحلقة تستمرّ حتى end_turn. عمليًا يقوم النموذج عادةً بـ 1–3 دورات بحث/استخلاص قبل الإجابة.
كيف أتأكّد أنه يستشهد بالمصادر فعلًا؟
أداتان للتحكّم. الأولى: في مطالبة النظام، اشترِط صراحةً '[source: URL]' داخليًا بعد كل ادّعاء. الثانية: استجابة URL Extract تتضمّن الرابط، فعندما يكون لدى النموذج في سياقه يميل إلى الاستشهاد طبيعيًا. وإن تسرّبت الاستشهادات، أضف تمريرة تنسيق نهائية ترفض الإجابة وتطلب من النموذج إضافة الاستشهادات.
ماذا عن زمن الاستجابة؟
كل جولة بحث ذهابًا وإيابًا نحو ثانية واحدة؛ وكل دفعة استخلاص 1–4 ثوانٍ حسب عدد الروابط وعرض JavaScript. نداء 'ابحث عن سؤال' نموذجي يقع في 5–15 ثانية بالوقت الفعلي. إن احتجت أسرع، وازِ خطوة الاستخلاص (الاستخلاص يأخذ قائمة روابط في نداء واحد أصلًا) وقيّد النموذج بجولة أداة واحدة لكل سؤال عبر تعليمات مطالبة النظام.
الواجهات البرمجية المستخدمة في هذا المقال
سارة تشوي هي الرئيسة التنفيذية لشركة API Pick. تكتب عن بناء واجهات برمجية جاهزة للإنتاج لوكلاء الذكاء الاصطناعي وسير عمل نماذج اللغة.