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

# Dateipfad: personal_assistant.py
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, weilthread_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:

MerkmalImplementierung
Multi-Tool-ZusammenarbeitWetter + Zeitplan + E-Mail; der Agent wählt automatisch die Aufrufreihenfolge
Strukturierte AusgabeDailySummary-Pydantic-Modell + to_markdown() als Markdown-Text gerendert
Streaming-Ausgabeagent.stream(stream_mode="values"), wobei Tool-Aufrufe und der Denkprozess schrittweise angezeigt werden
KonversationsgedächtnisSqliteSaver + 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 InteraktionBenutzer beschreiben ihre Anforderungen in natürlicher Sprache, und der Agent plant autonom
Andere Erweiterungen