LangChain Multi-Tool-Persönlicher Assistent
Dieser Artikel erstellt einen persönlichen Assistenten-Agenten, der Wetterabfrage, Terminverwaltung und E-Mail-Versand integriert und die vollständige Verwendung von Multi-Tool-Zusammenarbeit und strukturierter Ausgabe demonstriert.
Systemdesign
- Drei Werkzeuge: Wetterabfrage, Terminverwaltung, E-Mail-Versand
- Strukturierte Ausgabe: Zeitplan-Zusammenfassung im Markdown-Format
- Streaming-Ausgabe: Echtzeitanzeige des KI-Denkens und der Verarbeitung
- Gesprächsgedächtnis: Benutzereinstellungen und Kontext merken
Vollständiger Code
Vor dem Ausführen Abhängigkeiten installieren und.enves ordnungsgemäß konfigurieren.DEEPSEEK_API_KEY:
pip install langchain langchain-deepseek langgraph-checkpoint-sqlite python-dotenv
Beispiel
from dotenv import load_dotenv
load_dotenv()
import sqlite3
from datetime import datetime
from pydantic import BaseModel, Field
from langchain.tools import tool
from langchain.agents import create_agent
from langchain.agents.middleware import dynamic_prompt
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage
from langgraph.checkpoint.sqlite import SqliteSaver
# ========== 1. Mock-Daten ==========
calendar_events = [
{"id": 1, "title": "Python-Studium", "date": "2024-03-25",
"time": "14:00", "duration": "2 Stunden"},
{"id": 2, "title": "Wöchentliches Team-Meeting", "date": "2024-03-25",
"time": "10:00", "duration": "1 Stunde"},
{"id": 3, "title": "Code-Review", "date": "2024-03-26",
"time": "15:00", "duration": "1.5 Stunden"},
]
weather_db = {
"Hangzhou": {"condition": "Sonnig", "temp": 25, "humidity": 60},
"Beijing": {"condition": "Bewölkt", "temp": 18, "humidity": 45},
"Shanghai": {"condition": "Leichter Regen", "temp": 22, "humidity": 80},
}
# ========== 2. Werkzeuge definieren ==========
@tool
def get_weather(city: str) -> str:
"""Ruft das Echtzeitwetter einer angegebenen Stadt ab.
Argumente:
city: Stadtname, wie z. B. Hangzhou, Beijing, Shanghai
"""
data = weather_db.get(city)
if not data:
return f"Wetter für {city} wird derzeit nicht unterstützt. Unterstützte Städte: {', '.join(weather_db.keys())}"
return (f"Wetter für {city}: {data['condition']},"
f"Temperatur {data['temp']}°C, Luftfeuchtigkeit {data['humidity']}%")
@tool
def query_schedule(date: str = None) -> str:
"""Fragt den Zeitplan für ein angegebenes Datum ab. Wenn kein Datum angegeben ist, wird der heutige Zeitplan abgefragt.
Argumente:
date: Date, Format YYYY-MM-DD, z. B. 2024-03-25. Wenn nicht angegeben, heute abfragen
"""
if date is None:
date = datetime.now().strftime("%Y-%m-%d")
events = [e for e in calendar_events if e["date"] == date]
if not events:
return f"{date} hat keinen Zeitplan."
events.sort(key=lambda e: e["time"])
# Hier muss das Emoji-Zeichen direkt geschrieben werden, nicht eine HTML-Entität wie 📅 —
# Dies ist ein Python-String; er wird unverändert ausgegeben und nicht vom Browser in eine Grafik umgewandelt.
lines = [f"📅 {date} Zeitplan:"]
for e in events:
lines.append(f" - {e['time']} {e['title']}({e['duration']})")
return "\n".join(lines)
@tool
def send_email(to: str, subject: str, body: str) -> str:
"""E-Mail senden (simuliert).
Argumente:
to: E-Mail des Empfängers
subject: E-Mail-Betreff
body: E-Mail-Text
"""
# Senden simulieren
email_id = f"MSG-{datetime.now().strftime('%Y%m%d%H%M%S')}"
return f"E-Mail gesendet! Empfänger: {to}, Betreff: {subject}, E-Mail-ID: {email_id}"
# ========== 3. Strukturiertes Ausgabemodell ==========
class DailySummary(BaseModel):
"""Tägliche Zusammenfassung"""
date: str = Field(description="Datum")
weather_summary: str = Field(description="Wetterüberblick")
event_count: int = Field(description="Ereignisanzahl")
key_events: list[str] = Field(description="Liste wichtiger Ereignisse")
suggestion: str = Field(description="Heutiger Vorschlag")
def to_markdown(summary: DailySummary) -> str:
"""Die strukturierte DailySummary als Markdown-Text rendern,
entspricht dem Element "Strukturierte Ausgabe: Zeitplan-Zusammenfassung als Markdown formatiert" im Systemdesign."""
lines = [
f"## {summary.date} Heutige Zusammenfassung",
"",
f"- **Wetter**: {summary.weather_summary}",
f"- **Ereignisanzahl**: {summary.event_count}",
"",
"**Wichtige Punkte:**",
]
if summary.key_events:
lines += [f"- {event}" for event in summary.key_events]
else:
lines.append("- None")
lines += ["", f"**Heutiger Vorschlag**: {summary.suggestion}"]
return "\n".join(lines)
# ========== 4. Middleware definieren ==========
@dynamic_prompt
def inject_date_context(request) -> str:
"""Aktuelle Datumsinformationen dynamisch nach dem System-Prompt anhängen.
Der vorherige Ansatz, die Datumsnachricht mit @before_model in die Nachrichtenliste einzufügen, hatte zwei Probleme: 72. 1. Das von before_model zurückgegebene Nachrichten-Update durchläuft den add_messages-Reducer, der nur
1. Die von before_model zurückgegebenen Nachrichten-Updates durchlaufen den add_messages-Reducer; der Reducer arbeitet nur gemäß
"fügt neue Nachrichten am Ende an", ignoriert die Position in der zurückgegebenen Liste; insert(-1, ...)
die Absicht, vor der Benutzernachricht einzufügen, wird tatsächlich nicht wirksam;
2. before_model wird in der mehrrundigen Tool-Aufruf-Schleife wiederholt ausgelöst und fügt diese Nachricht leicht
zwischen AIMessage(tool_calls=...) und dem zugehörigen ToolMessage ein,
wodurch die strikte Anforderung an die Nachrichtenreihenfolge gestört wird.
Verwende stattdessen @dynamic_prompt, um die System-Prompt-Zeichenfolge direkt neu zu schreiben; jeder Modellaufruf wird
sie neu berechnen, ohne die Nachrichtenliste zu berühren, sodass kein Risiko der Anhäufung oder Fehlausrichtung besteht.
"""
now = datetime.now()
weekday = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"][jetzt.weekday()]
date_hint = (f"\n\n[System-Prompt] Aktuelles Datum ist {now.strftime('%Y-%m-%d')},
f"Wochentag {weekday}. Wenn der Benutzer kein Datum angibt, frage standardmäßig das heutige Datum ab.")
return request.system_prompt + date_hint
# ========== 5. Agent erstellen ==========
# Verwende SqliteSaver, um Konversationen zu persistieren, und implementiere damit das "Konversationsgedächtnis" im Systemdesign.
# Stelle die Verbindung selbst her und übergib sie an den SqliteSaver-Konstruktor, anstatt
# SqliteSaver.from_conn_string() (das ist ein Kontextmanager, der nur für with-Anweisungen geeignet ist,
# ein Kontextmanager, der nach der Verwendung geschlossen wird; siehe den Artikel "LangChain Intelligenter Kundenservice-Roboter".
conn = sqlite3.connect("personal_assistant.db", check_same_thread=False)
checkpointer = SqliteSaver(conn)
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0.3)
agent = create_agent(
model=model,
tools=[get_weather, query_schedule, send_email],
middleware=[inject_date_context],
response_format=DailySummary,
checkpointer=checkpointer,
system_prompt="""Du bist ein persönlicher Assistent namens "Xiao Zhu". Du kannst das Wetter prüfen, Termine verwalten und E-Mails senden.
## Funktionsweise
1. Wenn der Benutzer fragt "Wie ist heute?" oder ähnliche Fragen:
- Zuerst das heutige Wetter abfragen (get_weather)
- Dann den heutigen Zeitplan abfragen (query_schedule)
- Dann eine Tageszusammenfassung erstellen
2. Wenn der Benutzer bittet, eine E-Mail zu senden, verwende das send_email-Tool
3. Wenn der Benutzer nur nach dem Wetter oder nur nach dem Zeitplan fragt, rufe nur das entsprechende Tool auf
## Stil
- Ton ist freundlich und natürlich
- Bevorzuge die Verwendung von Tools, um Echtzeitdaten zu erhalten; erfinde nichts""",
)
# ========== 6. Interaktionsfunktion ==========
def chat(message: str, thread_id: str = "xiaoming"):
"""Mit dem Assistenten chatten:
- Verwende stream_mode="values", um die Denk- und Verarbeitungsprozesse der KI zu streamen (entspricht der "Streaming-Ausgabe")
- Übergib thread_id; Unterhaltungen unter derselben thread_id werden von SqliteSaver gespeichert (entspricht dem "Konversationsgedächtnis")
- Wenn in dieser Runde eine strukturierte Zusammenfassung erzeugt wird, wird sie als Markdown gerendert und ausgegeben (entspricht der "strukturierten Ausgabe")
"""
config = {"configurable": {"thread_id": thread_id}}
print(f"\n{'='*60}")
print(f"Du: {message}")
print(f"{'='*60}")
seen = 0
final_state = {}
for state in agent.stream(
{"messages": [HumanMessage(content=message)]},
config=config,
stream_mode="values",
):
final_state = state
msgs = state.get("messages", [])
# Jedes Mal, wenn ein paar weitere neue Nachrichten erscheinen, bedeutet das, dass der Agent einen Schritt vorangekommen ist; gib sie in Echtzeit aus
for msg in msgs[seen:]:
if msg.type == "ai" and getattr(msg, "tool_calls", None):
for call in msg.tool_calls:
print(f"🤔 Tool-Aufruf beschlossen: {call['name']}, Argumente: {call['args']}")
elif msg.type == "tool":
tool_name = getattr(msg, "name", "") or "Tool"
print(f"🔧 {tool_name} hat zurückgegeben: {str(msg.content)[:80]}")
elif msg.type == "ai" and msg.content:
print(f"🤖 Assistent: {msg.content}")
seen = len(msgs)
# Hinweis: response_format wird auf Agent-Ebene konfiguriert; jeder Aufruf (einschließlich des Sendens von E-Mails,
# Folgeanfragen, die nicht mit "schedule summary" zusammenhängen) werden zwangsweise versuchen, eine DailySummary zu generieren,
# Dies ist eine bekannte Einschränkung des globalen response_format. Um wirklich einen Multi-Intent-Assistenten zu erstellen, ist ein besserer Ansatz
# "generate daily summary" zu einem separaten Tool zu machen, sodass der Agent entscheiden kann, ob er es aufruft,
# anstatt ein immer aktives Ausgabeschema an den gesamten Agenten anzuhängen.
if "structured_response" in final_state:
summary = final_state["structured_response"]
print("\n--- Strukturierte Zusammenfassung (Markdown) ---)
print(to_markdown(summary))
return final_state
# ========== 7. Testen ==========
if __name__ == "__main__":
chat(Wie ist das Wetter heute in Hangzhou? Prüfe meinen Zeitplan und gib mir dann eine Zusammenfassung für heute)
chat(Hilf mir, eine E-Mail an [email protected] zu senden, mit dem Betreff 'Tageszusammenfassung' und dem Inhalt, dass der heutige Zeitplan bestätigt wurde)
# Dritte Runde: keine neuen Informationen, reiner Test, ob sich der Agent an die vorherige Konversation erinnert —
# Weil dieselbe thread_id übergeben wird, wird der Checkpointer die vollständige Historie zurückbringen
chat(Wie lautete der Betreff der E-Mail, um deren Versand ich dich gerade gebeten habe?)
Ausführungsergebnis:
============================================================
You: 杭州今天天气怎么样?看看我的日程,然后给我一个今日总结
============================================================
🤔 决定调用工具: get_weather,参数: {'city': '杭州'}
🔧 get_weather 返回: 杭州天气:晴,温度 25°C,湿度 60%
🤔 决定调用工具: query_schedule,参数: {}
🔧 query_schedule 返回: 📅 2024-03-25 日程安排: - 10:00 团队周会(1小时) - 14:00...
🤖 助手: 早上好!今天杭州是大晴天,气温 25°C,湿度 60%,很适合出门活动。
今天您有两项日程:上午 10:00 的团队周会和下午 14:00 的 Python 学习,祝您今天顺利!
--- 结构化摘要(Markdown) ---
## 2024-03-25 今日摘要
- **天气**:杭州晴,25°C,湿度 60%
- **日程数量**:2
**重要事项:**
- 10:00 团队周会
- 14:00 Python 学习
**今日建议**:上午先参加团队周会,下午集中精力学习 Python,注意劳逸结合
============================================================
You: 帮我发一封邮件给 [email protected],主题是'今日总结',内容是今天日程已确认
============================================================
🤔 决定调用工具: send_email,参数: {'to': '[email protected]', 'subject': '今日总结', 'body': '今天日程已确认'}
🔧 send_email 返回: 邮件已发送!收件人:[email protected],主题:今日总结,邮件ID:MSG-20240325143000
🤖 助手: 邮件已经发送成功啦!收件人是 [email protected],主题"今日总结"。
--- 结构化摘要(Markdown) ---
## 2024-03-25 今日摘要
...(这里的摘要和邮件内容其实没什么关系,是 response_format 强制生成的,
属于前面提到的已知局限)
============================================================
You: 我刚才让你发的那封邮件,主题是什么来着?
============================================================
🤖 助手: 你刚才让我发的那封邮件主题是"今日总结",收件人是 [email protected].
In der dritten Runde wurden überhaupt keine Tool-Aufrufe ausgelöst. Der Assistent konnte allein deshalb antworten, weil
thread_iddieselbe, SqliteSaver brachte die vollständige Nachrichtenhistorie aus den ersten beiden Runden zurück — genau so sieht "Konversationsgedächtnis" in Aktion wirklich aus. Wenn duthread_idsie auf einen neuen Wert änderst, wird die dritte Runde zu einer isolierten Konversation, und der Assistent weiß nicht, was "gerade eben" passiert ist.
Projektzusammenfassung
Dieser persönliche Assistent demonstriert:
| Merkmal | Implementierung |
|---|---|
| Multi-Tool-Zusammenarbeit | Wetter + Zeitplan + E-Mail; der Agent wählt automatisch die Aufrufreihenfolge |
| Strukturierte Ausgabe | DailySummary-Pydantic-Modell + to_markdown() als Markdown-Text gerendert |
| Streaming-Ausgabe | agent.stream(stream_mode="values"), wobei Tool-Aufrufe und der Denkprozess schrittweise angezeigt werden |
| Konversationsgedächtnis | SqliteSaver + thread_id, Kontext über mehrere Aufrufrunden hinweg merken |
| Datumsinjektion | @dynamic_prompt schreibt den System-Prompt dynamisch um und vermeidet eine Verunreinigung des Nachrichtenverlaufs |
| Natürlichsprachliche Interaktion | Benutzer beschreiben ihre Anforderungen in natürlicher Sprache, und der Agent plant autonom |