
KI-Vertragsmanagement-Software automatisiert den gesamten Vertragslebenszyklus – vom Entwurf über Überprüfung, Verhandlung, Genehmigung, Finalisierung bis zur Nachverfolgung von Verpflichtungen nach der Unterzeichnung – indem sie KI-Sprachverständnis mit deterministischer Dokumentenverarbeitung in einer .NET-Anwendung kombiniert. Der Unterschied zur Einzelaufgaben-Vertragsautomatisierung ist der Lebenszyklusumfang: Ein Vertragsprüfungstool markiert riskante Klauseln; ein Vertragsmanagementsystem bewegt eine Vereinbarung durch jede Phase, führt einen Prüfpfad über Versionen hinweg und verfolgt Verpflichtungen nach der Unterzeichnung.
Spire.Agent.Office ist ein Dokument-KI-Agent-SDK, das sowohl die Sprachverständnis- als auch die Dokumentenverarbeitungsschicht bereitstellt. Dieser Artikel zeigt, wie man ein Vertragslebenszyklus-Managementsystem in C# erstellt, das fünf Phasen zu einer Pipeline verkettet, mit Fehlerisolierung pro Phase, Format-Dispatch für PDF und Word und ordnungsgemäßer Statusverfolgung – Muster, die Einzelaufgabenbeispiele nicht abdecken.
Dieser Artikel präsentiert eine Referenzimplementierung einer KI-gestützten Vertragslebenszyklus-Pipeline in .NET. Ein Produktions-CLM-System würde zusätzlich Workflow-Persistenz, Identitäts- und Zugriffskontrolle, E-Signatur-Integration, Dokumentenspeicherung und Versionsaufbewahrung, Benachrichtigungen und Audit-Infrastruktur benötigen.
1. Der Vertragslebenszyklus: Fünf Phasen, in denen KI handelt
Ein Vertragslebenszyklus ist keine einzelne Dokumentoperation. Er ist eine Abfolge von Phasen, jede mit eigenen Eingaben, Ausgaben und Fehlermodi. Zu verstehen, wo KI in jeder Phase Mehrwert bietet – und wo deterministischer Code die Linie halten muss – ist die Grundlage eines Systems, das in der Produktion Bestand hat.
| Phase | Was geschieht | KI-Rolle | Rolle des deterministischen Codes |
|---|---|---|---|
| Entwurf | Einen Vertrag aus einer Vorlage plus strukturierten Daten generieren | Die Anfrage interpretieren, Vorlagenfelder auswählen und ausfüllen | Vorlage laden, Formatierung beibehalten, als .docx oder .pdf speichern |
| Überprüfung | Den Vertrag lesen, riskante Klauseln markieren, Schlüsselbegriffe extrahieren | Semantische Analyse der Klauselsprache, Risikobewertung | Strukturierten Prüfbericht schreiben, Prüfcheckliste durchsetzen |
| Verhandlung | Versionen vergleichen, Änderungen verfolgen, Änderungen zusammenführen | Deltas zusammenfassen, substanzielle vs. Formatierungsänderungen markieren | Änderungen verfolgen ein/aus, Dokumente vergleichen, Revisionen akzeptieren/ablehnen |
| Genehmigung | An Stakeholder weiterleiten, Zustimmungen sammeln | Genehmiger basierend auf Vertragstyp und -wert vorschlagen | Routing-Regeln durchsetzen, Prüfpfad aufzeichnen, finalisierte Kopie erstellen |
| Nach Unterzeichnung | Verpflichtungen, Fristen, Verlängerungen verfolgen | Verpflichtungen und Schlüsseldaten aus dem finalisierten Vertrag extrahieren | Strukturierte Metadaten speichern, Erinnerungen auslösen, Berichte generieren |
Der Lebenszyklus ist in der Praxis nicht linear – Verhandlungen führen zurück zur Überprüfung, Änderungen starten den Entwurf neu – aber die Pipeline-Architektur bewältigt dies durch Phasen-Routing statt einer festen Reihenfolge.
Was dies von einer reinen Vertragsprüfung unterscheidet
Die Automatisierung der Vertragsprüfung – das Thema von KI-Vertragsprüfung in C# – deckt eine Phase ab: das Lesen einer Vereinbarung und das Markieren von Problemen. Ein Vertragsmanagementsystem muss:
- Phasen miteinander verketten, wobei Daten von einer zur nächsten fließen (die Ausgabe des Entwurfs wird zur Eingabe der Überprüfung).
-
Mehrere Dokumentformate verarbeiten – Vorlagen kommen als
.docx, Gegenparteien senden möglicherweise.pdf, und Phasen mit Format-Dispatch verarbeiten beide ohne Ausnahme. - Zustand über Phasen hinweg beibehalten – der Prüfstatus eines Vertrags, die Anzahl der Verhandlungsversionen und die Genehmigungskette müssen zwischen Pipeline-Läufen erhalten bleiben.
- Fehler pro Vertrag isolieren – eine beschädigte Datei in einem Stapel von 200 darf nicht die gesamte Pipeline anhalten.
Diese Anforderungen prägen die folgende Architektur.
2. Systemarchitektur für die Automatisierung des Vertragslebenszyklus
Ein Vertragslebenszyklus-Managementsystem hat drei Schichten. Jede Schicht hat eine bestimmte Verantwortung, und die Grenzen zwischen ihnen sind der Ort, an dem Produktionsfehler entweder abgefangen werden oder durchrutschen.

Schicht 1: Instruktionsschnittstelle
Der Einstiegspunkt ist eine natürlichsprachliche Anweisung, die das gewünschte Ergebnis beschreibt – nicht die mechanischen Schritte. "Erstelle einen Lieferantenvertrag für Acme Corp unter Verwendung der Standardvorlage, überprüfe ihn auf nicht standardmäßige Zahlungsbedingungen und leite ihn an die Rechtsabteilung weiter, wenn die Haftungsobergrenze 500.000 $ überschreitet." Die Instruktionsschicht parst dies in einen Pipeline-Plan: welche Phasen ausgeführt werden, in welcher Reihenfolge und welche Parameter jede Phase benötigt.
Schicht 2: Phasen-Orchestrator
Der Orchestrator verwaltet den Fluss zwischen den Phasen. Er hält ein gemeinsames ContractContext-Objekt, das Vertragsmetadaten, das aktuelle Dokument und Phasenergebnisse von einer Phase zur nächsten trägt. Jede Phase erhält den Kontext, führt ihre Operation aus und gibt einen aktualisierten Kontext plus ein Phasenergebnis zurück. Der Orchestrator entscheidet anhand des Ergebnisses, ob fortgefahren, wiederholt oder an einen Ausnahmehandler weitergeleitet wird.
Schicht 3: Dokumentenverarbeitung
Jede Phase ruft Spire.Agent.Office auf, um die eigentliche Dokumentoperation durchzuführen. Der Agent übernimmt die KI-Schlussfolgerung (Verstehen der Anweisung, Extrahieren von Informationen) und die Dokumentschicht übernimmt die Dateioperationen (Laden, Ändern, Speichern). Format-Dispatch wird in Phasen angewendet, in denen mehrere Dokumentformate erwartet werden: Eine .pdf-Eingabe verwendet PdfDocument, während eine .docx-Eingabe Document verwendet.
Der Vertragskontext
Das gemeinsame Zustandsobjekt, das durch die Pipeline fließt:
/// <summary>
/// Shared state that flows through every lifecycle stage.
/// Each stage reads from and writes to this context.
/// </summary>
public class ContractContext
{
// Identity
public string ContractId { get; set; } = string.Empty;
public string ContractType { get; set; } = string.Empty; // "NDA", "MSA", "Vendor", etc.
// Document state
public string CurrentFilePath { get; set; } = string.Empty;
public string WorkDir { get; set; } = string.Empty;
public int VersionNumber { get; set; } = 1;
// Stage results
public DraftResult? Draft { get; set; }
public ReviewResult? Review { get; set; }
public NegotiationResult? Negotiation { get; set; }
public ApprovalResult? Approval { get; set; }
public ObligationResult? Obligations { get; set; }
// Pipeline metadata
public string Status { get; set; } = PipelineStages.Pending;
public List<string> StageLog { get; set; } = new();
public string? ErrorMessage { get; set; }
}
/// <summary>
/// Stage status constants. Using named constants instead of raw strings
/// prevents the "Status never set" bug where results fall through all
/// reporting buckets and silently disappear from batch summaries.
/// </summary>
public static class PipelineStages
{
public const string Pending = "Pending";
public const string Drafted = "Drafted";
public const string Reviewed = "Reviewed";
public const string Negotiated = "Negotiated";
public const string Approved = "Approved";
public const string Executed = "Executed"; // In this sample: finalized post-approval PDF, not e-signature
public const string Monitored = "Monitored";
public const string Failed = "Failed";
public const string NeedsReview = "NeedsReview";
}
Das Status-Feld verwendet benannte Konstanten anstelle von Rohstrings. Dies verhindert einen häufigen Fehlermodus, bei dem der Status nie explizit gesetzt wird, wodurch Dokumente durch alle Berichts-Buckets fallen und aus Stapelzusammenfassungen verschwinden.
3. Phase 1: Entwurf aus Vorlage und Daten
Die Entwurfsphase nimmt eine Vorlage (.docx mit {{Placeholder}}-Markierungen) und eine Datenquelle (Excel-Tabelle oder strukturierte Eingabe) und erzeugt dann einen befüllten Vertrag. Der KI-Agent liest die Vorlagenstruktur und füllt Platzhalter mit Daten aus der Quelle – eine Anweisung ersetzt den Feldzuordnungscode, den ein traditionelles SDK erfordert.
using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Doc;
public class DraftResult
{
public string OutputPath { get; set; } = string.Empty;
public int ContractsGenerated { get; set; }
public List<string> PlaceholdersFilled { get; set; } = new();
}
public class DraftingStage
{
private readonly AIOptions _options;
public DraftingStage(AIOptions options) => _options = options;
public DraftResult Execute(ContractContext context, string templatePath, string dataSourcePath)
{
// Format dispatch: verify the template is .docx before loading
if (!templatePath.EndsWith(".docx", StringComparison.OrdinalIgnoreCase))
throw new NotSupportedException(
$"Drafting requires a .docx template. Received: {templatePath}");
string[] attachments = { dataSourcePath };
using (Document template = new Document())
{
template.LoadFromFile(templatePath);
string draftPath = Path.Combine(context.WorkDir, $"{context.ContractId}-v1-draft.docx");
string draftInstruction =
"Read the data source and fill every {{Placeholder}} field in this template " +
"with the corresponding data. Preserve the template's layout, styling, and " +
"clause numbering. Save the completed contract to: " +
$"{draftPath}. Contract type: {context.ContractType}.";
AIResult result = template.AI(_options).ExecuteInstruction(
template, draftInstruction, draftPath, attachments);
if (result == null || !result.Success)
throw new InvalidOperationException(
$"Drafting failed: {result?.ErrorMessage ?? "Unknown error"}");
// Collect the generated file: prefer the explicit output path,
// fall back to AIResult.OutputFiles (unreliable for some product types).
string? generated = File.Exists(draftPath)
? draftPath
: result.OutputFiles?.FirstOrDefault(p => File.Exists(p));
if (generated == null)
throw new InvalidOperationException(
"Drafting completed but no output file was found (neither at the requested " +
$"path '{draftPath}' nor in AIResult.OutputFiles).");
context.CurrentFilePath = generated;
context.VersionNumber = 1;
context.Status = PipelineStages.Drafted;
context.StageLog.Add($"Draft: generated contract at {generated}");
return new DraftResult
{
OutputPath = generated,
ContractsGenerated = 1,
PlaceholdersFilled = ExtractPlaceholderNames(templatePath)
};
}
}
private static List<string> ExtractPlaceholderNames(string templatePath)
{
// Quick scan of the template for {{...}} markers to report what was filled
var placeholders = new List<string>();
using (Document doc = new Document())
{
doc.LoadFromFile(templatePath);
string text = doc.GetText();
var matches = System.Text.RegularExpressions.Regex.Matches(
text, @"\{\{(\w+)\}\}");
foreach (System.Text.RegularExpressions.Match m in matches)
if (!placeholders.Contains(m.Groups[1].Value))
placeholders.Add(m.Groups[1].Value);
}
return placeholders;
}
}
Wichtige API-Aufrufe
-
Document.LoadFromFile()– lädt die.docx-Vorlage mit Platzhaltern -
template.AI(_options)– hängt den KI-Dokumentenprozessor an -
ExecuteInstruction(doc, instruction, outputPath, attachments)– füllt Platzhalter aus der Datenquelle; übergeben Sie einen expliziten absoluten Pfad, damit der Agent das Produkt an einen bekannten Ort schreibt - Produktsammlung: Prüfen Sie zuerst
File.Exists(outputPath);AIResult.OutputFilesist für einige Produkttypen unzuverlässig und sollte nur als Fallback dienen
Format-Dispatch
Die Phase validiert das Eingabeformat vor der Verarbeitung. Eine .pdf-Vorlage oder ein nicht unterstütztes Format wird explizit mit einer klaren Meldung abgelehnt, anstatt im Agenten mit einer undurchsichtigen Ausnahme fehlzuschlagen. Dieses Muster verhindert einen häufigen Fehlermodus, bei dem fehlender Format-Dispatch dazu führt, dass PDF-Eingaben tief in der Verarbeitungspipeline nicht behandelte Ausnahmen auslösen.
Einen Vertrag zu entwerfen ist der einfache Fall. Wenn dieselbe Vorlage für Dutzende von Datensätzen ausgefüllt werden muss – neue Mitarbeiter, Lieferanten, Verlängerungen – skaliert das Instruktionsmuster unverändert auf Stapelausgabe; Batch-Vertragsgenerierung mit Spire.Agent.Office behandelt sowohl die Serienbrief- als auch die Platzhalterersetzungs-Route und vergleicht, wo jede passt.
SDK-Hinweis: Wenn ein expliziter
outputPathanExecuteInstructionübergeben wird, schreibt das SDK möglicherweise auch eineoutput-<filename>-Kopie im selben Verzeichnis. Dieses Duplikat hat identischen Inhalt und kann nach der Verarbeitung sicher bereinigt werden. Alle Phasen, die Ausgabepfade angeben, sind betroffen.
Das Produkt dieser Phase ist das ausgefüllte Dokument selbst – die Lieferantenvertragsvorlage, bei der jeder Platzhalter aus der Lieferantenstammarbeitsmappe ersetzt wurde.

4. Phase 2: Überprüfung und Risikoanalyse
Die Überprüfungsphase liest den entworfenen Vertrag, identifiziert riskante oder nicht standardmäßige Klauseln und erstellt einen strukturierten Prüfbericht. Im Gegensatz zur Entwurfsphase kann die Eingabe .docx (interner Entwurf) oder .pdf (Papier der Gegenpartei) sein, daher muss die Phase an den richtigen Dokumenttyp weiterleiten.
using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Doc;
using Spire.Pdf;
public class ReviewResult
{
public string ReportPath { get; set; } = string.Empty;
public int ClausesAnalyzed { get; set; }
public int RiskFlags { get; set; }
public double RiskScore { get; set; } // ratio of flagged clauses to total (0.0 = none flagged, 1.0 = all flagged)
public List<string> FlaggedClauses { get; set; } = new();
}
public class ReviewStage
{
private readonly AIOptions _options;
// Risk threshold: contracts above this score require manual review.
// Using a named constant prevents the "threshold always passes" bug
// where a miscalculated score boundary lets everything auto-approve.
public const double AutoApproveThreshold = 0.3;
public const double ManualReviewThreshold = 0.6;
public ReviewStage(AIOptions options) => _options = options;
public ReviewResult Execute(ContractContext context)
{
string filePath = context.CurrentFilePath;
string reportPath = Path.Combine(context.WorkDir, $"{context.ContractId}-review.md");
string reviewInstruction =
"Review this contract and write a Markdown report with two sections:\n" +
"1. A table listing each major clause, its type, and a risk score (0-1).\n" +
"2. A bullet list of clauses that deviate from standard practice for a " +
$"standard {context.ContractType}. " +
"Flag any of: uncapped liability, automatic renewal without notice, " +
"broad indemnification, unilateral termination, or payment terms exceeding 60 days. " +
"Prefix each flagged clause bullet with 'FLAG:' so the parser can identify it. " +
$"Reference the source document as '{context.ContractId}', not as 'input.docx'.\n" +
$"Save the report to: {reportPath}";
AIResult result = DispatchByFormat(filePath, reviewInstruction, reportPath);
if (result == null || !result.Success)
throw new InvalidOperationException(
$"Review failed: {result?.ErrorMessage ?? "Unknown error"}");
// Parse the review report to extract structured data
string reportContent = File.ReadAllText(reportPath);
var review = ParseReviewReport(reportContent, reportPath);
// Route based on risk score (three-tier classification)
if (review.RiskScore >= ManualReviewThreshold)
context.Status = PipelineStages.NeedsReview;
else if (review.RiskScore >= AutoApproveThreshold)
{
// Middle zone: not clean enough to auto-approve, not risky enough to block
context.Status = PipelineStages.Reviewed;
context.StageLog.Add(
$"Review: risk score {review.RiskScore:F2} in warning zone " +
$"({AutoApproveThreshold}-{ManualReviewThreshold}); reviewed with warning");
}
else
context.Status = PipelineStages.Reviewed;
context.StageLog.Add(
$"Review: {review.ClausesAnalyzed} clauses, {review.RiskFlags} flags, " +
$"score {review.RiskScore:F2}, status={context.Status}");
return review;
}
/// <summary>
/// Dispatch to the correct document type based on file extension.
/// This prevents the "PDF throws exception" bug where a stage
/// only handles .docx and fails on counterparty PDFs.
/// </summary>
private AIResult DispatchByFormat(string filePath, string instruction, string outputPath)
{
string ext = Path.GetExtension(filePath).ToLowerInvariant();
return ext switch
{
".docx" or ".doc" => ProcessWord(filePath, instruction, outputPath),
".pdf" => ProcessPdf(filePath, instruction, outputPath),
_ => throw new NotSupportedException(
$"Review stage does not support format: {ext}")
};
}
private AIResult ProcessWord(string filePath, string instruction, string outputPath)
{
using (Document doc = new Document())
{
doc.LoadFromFile(filePath);
return doc.AI(_options).ExecuteInstruction(
doc, instruction, outputPath, Array.Empty<string>());
}
}
private AIResult ProcessPdf(string filePath, string instruction, string outputPath)
{
using (PdfDocument pdf = new PdfDocument())
{
pdf.LoadFromFile(filePath);
return pdf.AI(_options).ExecuteInstruction(
pdf, instruction, outputPath, Array.Empty<string>());
}
}
private static ReviewResult ParseReviewReport(string markdown, string reportPath)
{
var result = new ReviewResult { ReportPath = reportPath };
// Count table rows as clauses analyzed
var tableLines = markdown.Split('\n')
.Where(l => l.StartsWith("|") && !l.StartsWith("|---") && !l.StartsWith("| --"))
.Skip(1); // skip header
result.ClausesAnalyzed = tableLines.Count();
// Count bullet points prefixed with "FLAG:" as risk flags
var flagLines = markdown.Split('\n')
.Select(l => l.TrimStart())
.Where(l => l.StartsWith("-") || l.StartsWith("*"))
.Where(l => l.Substring(1).TrimStart()
.StartsWith("FLAG:", StringComparison.OrdinalIgnoreCase));
result.FlaggedClauses = flagLines.Select(l => l.Trim()).ToList();
result.RiskFlags = result.FlaggedClauses.Count;
// Risk score: ratio of flagged clauses to total, clamped to [0, 1]
result.RiskScore = result.ClausesAnalyzed > 0
? Math.Min(1.0, (double)result.RiskFlags / result.ClausesAnalyzed)
: 0.0;
return result;
}
}
Die DispatchByFormat-Methode ist die entscheidende Ergänzung. In früheren Pipeline-Implementierungen warf eine Phase, die nur .docx verarbeitete, eine InvalidOperationException aus, wenn eine Gegenpartei eine .pdf sendete. Der Switch-Ausdruck leitet an den richtigen Dokumenttyp weiter, bevor der Agent ausgeführt wird, sodass beide Formate erstklassige Eingaben sind.
Die Risikoschwellenwerte (AutoApproveThreshold, ManualReviewThreshold) sind benannte Konstanten mit dokumentierter Semantik. Die Lücke zwischen 0,3 und 0,6 schafft eine explizite "Überprüfung erforderlich"-Zone – Verträge, die weder sauber genug für eine automatische Genehmigung noch riskant genug für eine Blockierung sind. Dies verhindert den Threshold-Dead-Logic-Bug, bei dem jeder Vertrag dieselbe Prüfung besteht.
Prüfungsergebnisse werden als Bericht persistiert und nicht im Speicher zurückgegeben, sodass das Audit auf Klausel-Ebene unabhängig von dem Lauf, der es erzeugt hat, erhalten bleibt.

5. Phase 3: Verhandlung und Versionskontrolle
Verhandlung ist der Punkt, an dem Verträge den Besitzer wechseln. Die Gegenpartei markiert das Dokument – markiert Klauseln, passt Bedingungen an, fügt Bedingungen hinzu. Das System muss Versionen vergleichen, Änderungen verfolgen und dem Team helfen zu entscheiden, was akzeptiert wird.
Diese Phase nutzt zwei Funktionen aus der Spire.Doc-Dokumentschicht, die über die ExecuteInstruction des KI-Agenten hinausgehen:
- Änderungen verfolgen – Revisionverfolgung aktivieren, damit jede Bearbeitung sichtbar und zuordenbar ist
- Dokumentenvergleich – zwei Versionen vergleichen und ein Diff-Dokument generieren
using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Doc;
using Spire.Doc.Documents;
using System.Text.RegularExpressions;
public class NegotiationResult
{
public string ComparisonPath { get; set; } = string.Empty;
public int ChangesDetected { get; set; }
public int SubstantiveChanges { get; set; } // changes to clause text, not formatting
public int FormattingChanges { get; set; }
public string NegotiatedFilePath { get; set; } = string.Empty;
}
public class NegotiationStage
{
private readonly AIOptions _options;
public NegotiationStage(AIOptions options) => _options = options;
/// <summary>
/// Compare the current version with a counterparty's revised version.
/// Produces a diff document with all changes marked.
/// </summary>
public NegotiationResult CompareVersions(
ContractContext context, string counterpartyFilePath)
{
string comparisonPath = Path.Combine(
context.WorkDir, $"{context.ContractId}-v{context.VersionNumber}-comparison.docx");
// Load both versions
using (Document ourVersion = new Document())
using (Document theirVersion = new Document())
{
ourVersion.LoadFromFile(context.CurrentFilePath);
theirVersion.LoadFromFile(counterpartyFilePath);
// Compare: marks every difference in ourVersion as a tracked change
ourVersion.Compare(theirVersion, "Contract Management System");
// Save the comparison document
ourVersion.SaveToFile(comparisonPath, FileFormat.Docx2013);
}
// Use the AI agent to analyze the comparison and categorize changes
string analysisPath = Path.Combine(
context.WorkDir, $"{context.ContractId}-negotiation-analysis.md");
using (Document comparison = new Document())
{
comparison.LoadFromFile(comparisonPath);
string analyzeInstruction =
"Analyze this document comparison and write a Markdown report:\n" +
"1. On the first line, write 'Total changes: <N>' where N is the total count " +
"of categorized changes (substantive + formatting).\n" +
"2. List each change as a bullet line prefixed with 'SUBSTANTIVE:' or 'FORMATTING:' " +
"followed by the clause affected and the nature of the change.\n" +
"3. SUBSTANTIVE changes: clause text, numbers, dates. " +
"FORMATTING changes: style, spacing, font.\n" +
$"Save to: {analysisPath}";
AIResult result = comparison.AI(_options).ExecuteInstruction(
comparison, analyzeInstruction, analysisPath, Array.Empty<string>());
if (result == null || !result.Success)
throw new InvalidOperationException(
$"Negotiation analysis failed: {result?.ErrorMessage}");
}
var negotiation = ParseNegotiationReport(analysisPath);
negotiation.ComparisonPath = comparisonPath;
negotiation.NegotiatedFilePath = counterpartyFilePath;
context.VersionNumber++;
context.CurrentFilePath = counterpartyFilePath;
context.Status = PipelineStages.Negotiated;
context.StageLog.Add(
$"Negotiation: {negotiation.ChangesDetected} changes " +
$"({negotiation.SubstantiveChanges} substantive, " +
$"{negotiation.FormattingChanges} formatting), v{context.VersionNumber}");
return negotiation;
}
/// <summary>
/// Enable track changes on a contract before sending it for negotiation.
/// This ensures every edit by the counterparty is visible and attributable.
/// </summary>
public string PrepareForRedlining(ContractContext context)
{
string redlineReadyPath = Path.Combine(
context.WorkDir, $"{context.ContractId}-v{context.VersionNumber}-redline.docx");
using (Document doc = new Document())
{
doc.LoadFromFile(context.CurrentFilePath);
// Enable track changes so all edits are recorded as revisions
doc.TrackChanges = true;
doc.SaveToFile(redlineReadyPath, FileFormat.Docx2013);
}
context.StageLog.Add("Negotiation: track changes enabled for redlining");
return redlineReadyPath;
}
/// <summary>
/// Accept all tracked changes to produce a clean final version.
/// Optional post-review operation: not automatically invoked by the pipeline
/// because acceptance should occur only after human review of the changes.
/// </summary>
public string AcceptAllChanges(ContractContext context)
{
string cleanPath = Path.Combine(
context.WorkDir, $"{context.ContractId}-v{context.VersionNumber}-final.docx");
using (Document doc = new Document())
{
doc.LoadFromFile(context.CurrentFilePath);
doc.AcceptChanges();
doc.SaveToFile(cleanPath, FileFormat.Docx2013);
}
context.CurrentFilePath = cleanPath;
context.StageLog.Add("Negotiation: all changes accepted, clean version produced");
return cleanPath;
}
private static NegotiationResult ParseNegotiationReport(string reportPath)
{
string content = File.ReadAllText(reportPath);
var result = new NegotiationResult();
// Parse total changes from the report (try multiple patterns for robustness)
var totalMatch = Regex.Match(content, @"Total changes:\s*(\d+)", RegexOptions.IgnoreCase);
if (!totalMatch.Success)
totalMatch = Regex.Match(content, @"(\d+)\s+(?:tracked\s+)?changes", RegexOptions.IgnoreCase);
if (totalMatch.Success)
result.ChangesDetected = int.Parse(totalMatch.Groups[1].Value);
// Count changes by prefix: SUBSTANTIVE: or FORMATTING:
result.SubstantiveChanges = CountByPrefix(content, "SUBSTANTIVE:");
result.FormattingChanges = CountByPrefix(content, "FORMATTING:");
return result;
}
private static int CountByPrefix(string content, string prefix)
{
return content.Split('\n')
.Select(l => l.TrimStart())
.Where(l => l.StartsWith("-") || l.StartsWith("*"))
.Count(l => l.Substring(1).TrimStart()
.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
}
}
Wichtige API-Aufrufe
-
Document.Compare(otherDoc, authorName)– generiert ein Diff-Dokument, bei dem alle Änderungen als verfolgte Revisionen markiert sind (aus der Spire.Doc-Vergleichsdemo) -
Document.TrackChanges = true– aktiviert die Revisionverfolgung, damit jede Bearbeitung sichtbar und zuordenbar ist -
Document.AcceptChanges()– akzeptiert alle verfolgten Änderungen, um eine saubere endgültige Version zu erstellen -
doc.AI(_options).ExecuteInstruction(...)– analysiert den Vergleich und kategorisiert Änderungen
Die Verhandlungsphase kombiniert deterministische Dokumentoperationen (Vergleichen, Änderungen verfolgen, Akzeptieren) mit KI-Analyse (Kategorisierung von Änderungen als substanziell oder formatierend). Die deterministischen Operationen stammen aus der Spire.Doc-Schicht – dieselben APIs, die im Code-Referenzverzeichnis verfügbar sind – während der KI-Agent die semantische Kategorisierung übernimmt, die sonst eine manuelle Überprüfung erfordern würde.
Das Beispiel vergleicht Versionen und verfolgt den aktuellen Dateipfad; ein Produktionssystem würde normalerweise jede Version separat persistieren und explizit aufzeichnen, welche Revision akzeptiert wurde. AcceptAllChanges() wird als optionale Finalisierungsoperation nach der Verhandlung bereitgestellt, aber sie wird von der Beispiel-Pipeline nicht automatisch aufgerufen, da die Annahme erst nach menschlicher Überprüfung erfolgen sollte.
Was Document.Compare zurückgibt, ist ein Dokument, keine Zusammenfassung: Die unten verfolgten Revisionen sind die Markierungen, die ein Prüfer sonst von Hand zusammenstellen müsste.

6. Phase 4: Genehmigung und Finalisierung
Die Genehmigungsphase leitet den Vertrag basierend auf Vertragstyp und -wert an die richtigen Stakeholder weiter, zeichnet deren Zustimmungen auf und erstellt eine finalisierte PDF-Kopie, nachdem alle erforderlichen Genehmigungen aufgezeichnet wurden. Die Routing-Logik ist deterministisch – sie verwendet Geschäftsregeln, nicht KI – aber der Agent unterstützt, indem er Genehmiger vorschlägt und die Genehmigungszusammenfassung generiert.
using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Doc;
public class ApprovalResult
{
public List<ApprovalRecord> Approvals { get; set; } = new();
public bool AllApproved { get; set; }
public string ExecutedFilePath { get; set; } = string.Empty; // Finalized PDF path, not a signed contract
public DateTime? ExecutionDate { get; set; } // Finalization timestamp, not a signing date
}
public class ApprovalRecord
{
public string Approver { get; set; } = string.Empty;
public string Role { get; set; } = string.Empty;
public bool Approved { get; set; }
public DateTime Timestamp { get; set; }
public string? Comments { get; set; }
}
public class ApprovalStage
{
private readonly AIOptions _options;
public ApprovalStage(AIOptions options) => _options = options;
/// <summary>
/// Route the contract for approval based on type and value.
/// Routing rules are deterministic business logic, not AI.
/// </summary>
public ApprovalResult Execute(
ContractContext context, double contractValue,
Dictionary<string, bool>? approvalDecisions = null)
{
var routingPlan = DetermineApprovers(context.ContractType, contractValue);
var result = new ApprovalResult();
// Generate an approval summary for each approver
string summaryPath = Path.Combine(
context.WorkDir, $"{context.ContractId}-approval-summary.md");
using (Document doc = new Document())
{
doc.LoadFromFile(context.CurrentFilePath);
string summaryInstruction =
"Write a one-page approval summary for this contract: parties, value, " +
"key terms, risk flags from review, and changes from negotiation. " +
$"Save to: {summaryPath}";
AIResult aiResult = doc.AI(_options).ExecuteInstruction(
doc, summaryInstruction, summaryPath, Array.Empty<string>());
if (aiResult == null || !aiResult.Success)
throw new InvalidOperationException(
$"Approval summary generation failed: {aiResult?.ErrorMessage}");
}
// In a real system, this would integrate with an approval workflow API.
// approvalDecisions maps approver name → approved/rejected. A missing key
// means no decision was recorded — which is not the same as a rejection.
int approvedCount = 0, rejectedCount = 0, pendingCount = 0;
foreach (var approver in routingPlan)
{
bool decision = false;
bool hasDecision = approvalDecisions != null
&& approvalDecisions.TryGetValue(approver.Name, out decision);
bool approved = hasDecision && decision;
// Three states, all distinguishable in the audit record
string comment = !hasDecision ? "Pending approval"
: approved ? "Approved by approver"
: "Rejected by approver";
if (!hasDecision) pendingCount++;
else if (approved) approvedCount++;
else rejectedCount++;
result.Approvals.Add(new ApprovalRecord
{
Approver = approver.Name,
Role = approver.Role,
Approved = approved,
Timestamp = DateTime.UtcNow,
Comments = comment
});
}
result.AllApproved = result.Approvals.All(a => a.Approved);
if (result.AllApproved)
{
// Produce the finalized copy as PDF
string executedPath = Path.Combine(
context.WorkDir, $"{context.ContractId}-executed.pdf");
using (Document doc = new Document())
{
doc.LoadFromFile(context.CurrentFilePath);
doc.SaveToFile(executedPath, FileFormat.PDF);
result.ExecutedFilePath = executedPath;
result.ExecutionDate = DateTime.UtcNow;
}
context.CurrentFilePath = result.ExecutedFilePath;
context.Status = PipelineStages.Executed;
}
else
{
context.Status = PipelineStages.NeedsReview;
}
context.StageLog.Add(
$"Approval: {result.Approvals.Count} approvers, " +
$"approved={approvedCount}, rejected={rejectedCount}, " +
$"pending={pendingCount}, all approved={result.AllApproved}");
return result;
}
/// <summary>
/// Deterministic routing rules. These are business logic, not AI.
/// </summary>
private static List<(string Name, string Role)> DetermineApprovers(
string contractType, double value)
{
var approvers = new List<(string, string)>();
// All contracts need Legal sign-off (example business rule)
approvers.Add(("Legal Team", "Legal Counsel"));
// Contracts over $100K need VP approval (example business rule)
if (value > 100_000)
approvers.Add(("VP Operations", "VP"));
// Contracts over $500K need CFO approval (example business rule)
if (value > 500_000)
approvers.Add(("CFO", "CFO"));
// Vendor contracts need Procurement sign-off
if (contractType.Equals("Vendor", StringComparison.OrdinalIgnoreCase))
approvers.Add(("Procurement", "Procurement Manager"));
return approvers;
}
}
Die Routing-Regeln in DetermineApprovers sind absichtlich deterministisch. KI schlägt vor und fasst zusammen; sie entscheidet nicht, wer einen Vertrag unterzeichnet. Die Genehmigungskette ist eine Geschäftsregel, die auditierbar und konsistent sein muss – genau die Art von Entscheidung, die in Code gehört, nicht in ein Sprachmodell.
Hinweis: Der
Executed-Status in diesem Beispiel bedeutet die nach der Genehmigung erstellte endgültige PDF – keinen unterzeichneten Vertrag. Das tatsächliche elektronische Unterzeichnen – Signaturumschläge, Identitätsüberprüfung des Unterzeichners, Einbetten von Signaturfeldern – sollte von einem separaten, integrierten E-Signatur-Workflow gehandhabt werden.
Sobald die Entscheidung jedes Genehmigers aufgezeichnet ist, schreibt die Phase den finalisierten Vertrag als PDF – und diese PDF, nicht der Quellentwurf, ist das, was Phase 5 verwendet.

7. Phase 5: Überwachung nach Unterzeichnung
In einem Produktions-Workflow beginnt die Überwachung nach der Unterzeichnung, nachdem der Vertrag elektronisch oder anderweitig formell unterzeichnet wurde. In dieser Referenzimplementierung wird die von Phase 4 erstellte finalisierte PDF als Überwachungseingabe verwendet. Die Überwachung nach der Unterzeichnung extrahiert Verpflichtungen, Schlüsseldaten und Verlängerungsbedingungen aus dem Vertrag und speichert sie als strukturierte Metadaten, die nachgelagerte Systeme für Erinnerungen und Berichte nutzen.
using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Doc;
using Spire.Pdf;
public class ObligationResult
{
public List<Obligation> Obligations { get; set; } = new();
public List<KeyDate> KeyDates { get; set; } = new();
public bool AutoRenewal { get; set; }
public DateTime? RenewalDate { get; set; }
public string MetadataPath { get; set; } = string.Empty;
}
public class Obligation
{
public string Description { get; set; } = string.Empty;
public string Party { get; set; } = string.Empty; // who must fulfill
public string Frequency { get; set; } = string.Empty; // "monthly", "annual", "one-time"
public DateTime? DueDate { get; set; }
public string DueDateRaw { get; set; } = string.Empty; // original deadline text from the contract
}
public class KeyDate
{
public string Description { get; set; } = string.Empty;
public DateTime Date { get; set; }
public string Type { get; set; } = string.Empty; // "renewal", "termination", "milestone"
}
public class MonitoringStage
{
private readonly AIOptions _options;
public MonitoringStage(AIOptions options) => _options = options;
public ObligationResult Execute(ContractContext context)
{
string metadataPath = Path.Combine(
context.WorkDir, $"{context.ContractId}-obligations.json");
string extractInstruction =
"Extract all post-signature obligations and key dates from this contract. " +
"Write a JSON file with:\n" +
"1. \"obligations\": array of {description, party, frequency, dueDate, dueDateRaw} " +
"where dueDate is an ISO-8601 date (YYYY-MM-DD). If the obligation has a relative " +
"deadline (e.g., 'Within 90 days of receipt'), convert it to an absolute date based " +
"on the contract effective date. Use null only for ongoing obligations with no " +
"fixed deadline. Always put the original deadline text in dueDateRaw.\n" +
"2. \"keyDates\": array of {description, date, type} where date is ISO-8601 " +
"(YYYY-MM-DD) and type is 'renewal', 'termination', or 'milestone'\n" +
"3. \"autoRenewal\": boolean\n" +
"4. \"renewalDate\": ISO-8601 date (YYYY-MM-DD) or null\n" +
$"Save to: {metadataPath}";
// Dispatch by format — executed contracts may be PDF or DOCX
AIResult result = DispatchExtraction(
context.CurrentFilePath, extractInstruction, metadataPath, _options);
if (result == null || !result.Success)
throw new InvalidOperationException(
$"Obligation extraction failed: {result?.ErrorMessage}");
var obligations = ParseObligationMetadata(metadataPath);
obligations.MetadataPath = metadataPath;
context.Obligations = obligations;
context.Status = PipelineStages.Monitored;
// Diagnostics: bucket every obligation by how its deadline came out, so that a
// field the agent writes but the parser fails to read cannot pass unnoticed.
int datesParsed = obligations.Obligations.Count(o => o.DueDate != null);
int datesUnparsed = obligations.Obligations.Count(
o => o.DueDate == null && !string.IsNullOrEmpty(o.DueDateRaw));
int datesOngoing = obligations.Obligations.Count(
o => o.DueDate == null && string.IsNullOrEmpty(o.DueDateRaw));
int datesNoSourceText = obligations.Obligations.Count(
o => o.DueDate != null && string.IsNullOrEmpty(o.DueDateRaw));
context.StageLog.Add(
$"Monitoring: {obligations.Obligations.Count} obligations, " +
$"{obligations.KeyDates.Count} key dates, " +
$"auto-renewal={obligations.AutoRenewal}, " +
$"dates parsed={datesParsed}, unparsed={datesUnparsed}, ongoing={datesOngoing}, " +
$"parsed-without-source-text={datesNoSourceText}");
return obligations;
}
private static AIResult DispatchExtraction(
string filePath, string instruction, string outputPath, AIOptions options)
{
string ext = Path.GetExtension(filePath).ToLowerInvariant();
return ext switch
{
".docx" or ".doc" => ProcessWordExtraction(filePath, instruction, outputPath, options),
".pdf" => ProcessPdfExtraction(filePath, instruction, outputPath, options),
_ => throw new NotSupportedException(
$"Obligation extraction does not support format: {ext}")
};
}
private static AIResult ProcessWordExtraction(
string filePath, string instruction, string outputPath, AIOptions options)
{
using (Document doc = new Document())
{
doc.LoadFromFile(filePath);
return doc.AI(options).ExecuteInstruction(
doc, instruction, outputPath, Array.Empty<string>());
}
}
private static AIResult ProcessPdfExtraction(
string filePath, string instruction, string outputPath, AIOptions options)
{
using (PdfDocument pdf = new PdfDocument())
{
pdf.LoadFromFile(filePath);
return pdf.AI(options).ExecuteInstruction(
pdf, instruction, outputPath, Array.Empty<string>());
}
}
private static ObligationResult ParseObligationMetadata(string jsonPath)
{
string json = File.ReadAllText(jsonPath);
using var doc = System.Text.Json.JsonDocument.Parse(json);
var root = doc.RootElement;
var result = new ObligationResult();
if (TryGetPropertyIgnoreCase(root, "obligations", out var obs))
foreach (var ob in obs.EnumerateArray())
{
string dueDateRaw = TryGetPropertyIgnoreCase(ob, "dueDateRaw", out var dr)
&& dr.ValueKind == System.Text.Json.JsonValueKind.String
? dr.GetString() ?? "" : "";
result.Obligations.Add(new Obligation
{
Description = TryGetPropertyIgnoreCase(ob, "description", out var d) ? d.GetString() ?? "" : "",
Party = TryGetPropertyIgnoreCase(ob, "party", out var p) ? p.GetString() ?? "" : "",
Frequency = TryGetPropertyIgnoreCase(ob, "frequency", out var f) ? f.GetString() ?? "" : "",
DueDate = TryGetDate(TryGetPropertyIgnoreCase(ob, "dueDate", out var dd2), dd2),
DueDateRaw = dueDateRaw
});
}
if (TryGetPropertyIgnoreCase(root, "keyDates", out var kds))
foreach (var kd in kds.EnumerateArray())
result.KeyDates.Add(new KeyDate
{
Description = TryGetPropertyIgnoreCase(kd, "description", out var d) ? d.GetString() ?? "" : "",
Date = TryGetDate(TryGetPropertyIgnoreCase(kd, "date", out var dt), dt) ?? default,
Type = TryGetPropertyIgnoreCase(kd, "type", out var t) ? t.GetString() ?? "" : ""
});
result.AutoRenewal = TryGetBool(TryGetPropertyIgnoreCase(root, "autoRenewal", out var ar), ar);
result.RenewalDate = TryGetDate(TryGetPropertyIgnoreCase(root, "renewalDate", out var rd), rd);
return result;
}
/// <summary>
/// Case-insensitive property lookup. Prevents silent data loss when
/// the AI agent outputs PascalCase or snake_case instead of camelCase.
/// </summary>
private static bool TryGetPropertyIgnoreCase(
System.Text.Json.JsonElement element, string name,
out System.Text.Json.JsonElement value)
{
foreach (var prop in element.EnumerateObject())
{
if (string.Equals(prop.Name, name, StringComparison.OrdinalIgnoreCase))
{
value = prop.Value;
return true;
}
}
value = default;
return false;
}
/// <summary>
/// Safe date parsing: handles null, non-string values, and invalid formats
/// without throwing. Returns null on any parse failure.
/// </summary>
private static DateTime? TryGetDate(bool exists, System.Text.Json.JsonElement element)
{
if (!exists || element.ValueKind != System.Text.Json.JsonValueKind.String)
return null;
string? s = element.GetString();
return DateTime.TryParse(s, out var date) ? date : null;
}
/// <summary>
/// Safe boolean parsing: handles string "true"/"false" and actual booleans
/// without throwing on type mismatch.
/// </summary>
private static bool TryGetBool(bool exists, System.Text.Json.JsonElement element)
{
if (!exists) return false;
if (element.ValueKind == System.Text.Json.JsonValueKind.True) return true;
if (element.ValueKind == System.Text.Json.JsonValueKind.False) return false;
if (element.ValueKind == System.Text.Json.JsonValueKind.String)
return bool.TryParse(element.GetString(), out var b) && b;
return false;
}
}
Die Verpflichtungsmetadaten werden als JSON gespeichert, nicht als Dokument. Dadurch sind sie für nachgelagerte Systeme abfragbar – ein Verlängerungs-Dashboard kann alle Verträge nach autoRenewal == true und renewalDate < DateTime.Now.AddDays(90) durchsuchen, ohne ein einziges Dokument zu öffnen. Verpflichtungen mit strukturierten dueDate-Werten können automatisch verfolgt werden; bei denen, deren Frist nicht in ein absolutes Datum umgewandelt werden konnte, bleibt der Originaltext in dueDateRaw für die manuelle Überprüfung erhalten. Der KI-Agent extrahiert die Informationen; die deterministische Schicht speichert sie in einem Format, das Geschäftssysteme nutzen können.
Das Extraktionsmuster ist dasselbe, das für Rechnungen in Rechnungsverarbeitung mit einem KI-Agenten in .NET automatisieren verwendet wird: strukturierte Felder aus einem eingehenden Dokument ziehen, sie gegen deterministische Regeln validieren und die unsicheren Werte zur Überprüfung weiterleiten. Verträge unterscheiden sich hauptsächlich darin, was danach passiert – Verpflichtungen werden zu langlebigen Datensätzen, die die Transaktion überleben, während Rechnungsfelder einmalig konsumiert werden.
Die extrahierten Daten werden als JSON und nicht als Prosa gespeichert, sodass jede Verpflichtung und jedes Schlüsseldatum den ursprünglichen Vertragswortlaut neben dem vom Parser abgeleiteten Wert trägt.

8. Orchestrierung der gesamten Pipeline
Der Orchestrator verkettet die Phasen miteinander und übergibt den ContractContext von einer zur nächsten. Seine zwei kritischen Verantwortlichkeiten sind Fehlerisolierung pro Vertrag (ein fehlgeschlagener Vertrag darf den Stapel nicht anhalten) und Statuserhaltung (jeder Vertrag muss in genau einem Berichts-Bucket landen).
using Spire.Agent.Office.AI;
public class PipelineResult
{
public string ContractId { get; set; } = string.Empty;
public string Status { get; set; } = PipelineStages.Pending;
public List<string> StageLog { get; set; } = new();
public string? ErrorMessage { get; set; }
}
public class BatchResult
{
public int Total { get; set; }
public int Successful { get; set; }
public int Flagged { get; set; } // needs review
public int Errored { get; set; }
public List<PipelineResult> Results { get; set; } = new();
}
public class ContractLifecyclePipeline
{
private readonly DraftingStage _drafting;
private readonly ReviewStage _review;
private readonly NegotiationStage _negotiation;
private readonly ApprovalStage _approval;
private readonly MonitoringStage _monitoring;
public ContractLifecyclePipeline(AIOptions options)
{
_drafting = new DraftingStage(options);
_review = new ReviewStage(options);
_negotiation = new NegotiationStage(options);
_approval = new ApprovalStage(options);
_monitoring = new MonitoringStage(options);
}
/// <summary>
/// Run a single contract through the full lifecycle.
/// Each stage is wrapped in its own try-catch so that a failure
/// in one stage produces a clear error without corrupting the
/// context for subsequent contracts in the batch.
/// </summary>
public PipelineResult RunSingle(
ContractContext context,
string templatePath,
string dataSourcePath,
double contractValue,
string? counterpartyFilePath = null,
Dictionary<string, bool>? approvalDecisions = null)
{
try
{
// Stage 1: Drafting
try
{
context.Draft = _drafting.Execute(context, templatePath, dataSourcePath);
}
catch (Exception ex)
{
return Fail(context, "Drafting", ex);
}
// Stage 2: Review
try
{
context.Review = _review.Execute(context);
}
catch (Exception ex)
{
return Fail(context, "Review", ex);
}
// If review flagged for manual review, stop here
if (context.Status == PipelineStages.NeedsReview)
return Flag(context, "Review flagged for manual review");
// Stage 3: Negotiation (only when a counterparty version exists)
if (counterpartyFilePath != null)
{
try
{
context.Negotiation = _negotiation.CompareVersions(context, counterpartyFilePath);
}
catch (Exception ex)
{
return Fail(context, "Negotiation", ex);
}
}
// Stage 4: Approval and Finalization
try
{
context.Approval = _approval.Execute(context, contractValue, approvalDecisions);
}
catch (Exception ex)
{
return Fail(context, "Approval", ex);
}
if (context.Status == PipelineStages.NeedsReview)
return Flag(context, "Approval incomplete");
// Stage 5: Post-signature monitoring
try
{
context.Obligations = _monitoring.Execute(context);
}
catch (Exception ex)
{
return Fail(context, "Monitoring", ex);
}
return new PipelineResult
{
ContractId = context.ContractId,
Status = context.Status,
StageLog = context.StageLog
};
}
catch (Exception ex)
{
return Fail(context, "Pipeline", ex);
}
}
/// <summary>
/// Process a batch of contracts with per-file error isolation.
/// One corrupt file does not halt the batch.
/// Approval decisions are keyed by contract id; a contract with no
/// entry keeps its approvers pending and stops at NeedsReview.
/// </summary>
public BatchResult RunBatch(
List<(ContractContext context, string template, string data, double value)> contracts,
Dictionary<string, Dictionary<string, bool>>? approvalDecisions = null)
{
var results = new List<PipelineResult>();
foreach (var (context, template, data, value) in contracts)
{
// Per-contract try-catch: one failure does not stop the batch
try
{
Dictionary<string, bool>? decisions = null;
if (approvalDecisions != null &&
approvalDecisions.TryGetValue(context.ContractId, out var d))
decisions = d;
results.Add(RunSingle(context, template, data, value, null, decisions));
}
catch (Exception ex)
{
results.Add(Fail(context, "Batch", ex));
}
}
// Status conservation: every result must land in exactly one bucket.
// Using the remainder method ensures that any result with an unexpected
// status lands in "Flagged" (conservative) rather than vanishing.
int successful = results.Count(r => r.Status == PipelineStages.Monitored
|| r.Status == PipelineStages.Executed);
int errored = results.Count(r => r.Status == PipelineStages.Failed);
return new BatchResult
{
Total = results.Count,
Successful = successful,
Flagged = results.Count - successful - errored, // remainder → conservative
Errored = errored,
Results = results
};
}
private static PipelineResult Fail(ContractContext context, string stage, Exception ex)
{
context.Status = PipelineStages.Failed;
context.ErrorMessage = $"{stage}: {ex.Message}";
context.StageLog.Add($"ERROR at {stage}: {ex.Message}");
return new PipelineResult
{
ContractId = context.ContractId,
Status = PipelineStages.Failed,
StageLog = context.StageLog,
ErrorMessage = context.ErrorMessage
};
}
private static PipelineResult Flag(ContractContext context, string reason)
{
context.StageLog.Add($"FLAGGED: {reason}");
return new PipelineResult
{
ContractId = context.ContractId,
Status = context.Status,
StageLog = context.StageLog
};
}
}
Fehlerisolierung pro Phase
Jede Phase ist in einen eigenen try-catch-Block eingewickelt. Wenn die Überprüfungsphase fehlschlägt, weil der KI-Agent eine besonders komplexe Klausel nicht parsen kann, zeichnet die Pipeline den Fehler auf und stoppt – aber der ContractContext für den nächsten Vertrag im Stapel bleibt unberührt. Dies verhindert einen häufigen Fehlermodus, bei dem eine einzelne beschädigte Datei in einem Stapel von 200 die gesamte Pipeline ohne Teilergebnisse anhält.
Statuserhaltung
Die Stapelzusammenfassung verwendet die Restmethode, um die Flagged-Anzahl zu berechnen: Flagged = Total - Successful - Errored. Dies ist eine strukturelle Eigenschaft – jedes Ergebnis, das nicht explizit erfolgreich oder fehlerhaft ist, landet in "flagged" (konservatives Routing zur manuellen Überprüfung). Dies verhindert den Bug, bei dem Ergebnisse mit unerwarteten Statuswerten stillschweigend aus allen drei Berichts-Buckets verschwinden.
Ausführen der Pipeline
// Configure the agent
AIOptions options = new AIOptions
{
WorkDir = @"C:\contract-mgmt\work",
SpireToken = Environment.GetEnvironmentVariable("SPIRE_TOKEN")!
};
// Create the pipeline
var pipeline = new ContractLifecyclePipeline(options);
// Prepare a batch of contracts
var batch = new List<(ContractContext, string, string, double)>
{
new(new ContractContext
{
ContractId = "CTR-2026-001",
ContractType = "Vendor",
WorkDir = @"C:\contract-mgmt\work\CTR-2026-001"
},
@"C:\templates\vendor-agreement.docx",
@"C:\data\vendors-q1.xlsx",
250_000)
};
// Run the batch. Approval decisions are supplied per contract id —
// a contract with no recorded decision stays in NeedsReview and
// never reaches Stage 5.
BatchResult result = pipeline.RunBatch(batch, new Dictionary<string, Dictionary<string, bool>>
{
["CTR-2026-001"] = new Dictionary<string, bool>
{
["Legal Team"] = true,
["VP Operations"] = true,
["Procurement"] = true
}
});
Console.WriteLine($"Total: {result.Total}");
Console.WriteLine($"Successful: {result.Successful}");
Console.WriteLine($"Flagged: {result.Flagged}");
Console.WriteLine($"Errored: {result.Errored}");
foreach (var r in result.Results)
{
Console.WriteLine($" {r.ContractId}: {r.Status}");
foreach (var log in r.StageLog)
Console.WriteLine($" {log}");
}
Genehmigungsentscheidungen werden dem Stapelaufruf übergeben und nicht von der Pipeline abgeleitet: Ein Vertrag ohne aufgezeichnete Entscheidung behält seine Genehmiger im Status "ausstehend", landet in Flagged und stoppt vor Phase 5 – das beabsichtigte Ergebnis für eine Vereinbarung, die noch niemand genehmigt hat.
Ein Lauf über eine Handvoll Verträge ist der einfache Fall. Stellen Sie dieselben Phasen hinter eine Dokumentenwarteschlange, und die Orchestrierungsfragen ändern sich: Nebenläufigkeitsgrenzen, Fehlerisolierung pro Dokument, Wiederholungsrichtlinie und Aggregation in einen Stapelbericht. Intelligente Dokumentenverarbeitung in .NET: Aufbau von IDP-Pipelines arbeitet diese auf Pipeline-Ebene durch, und der Stapelabschnitt dieses Artikels ist eine vertragsspezifische Instanz desselben Musters.
9. Wo KI endet und Governance beginnt
Die obige Architektur weist dem KI-Agenten bestimmte Verantwortlichkeiten und dem deterministischen Code bestimmte Verantwortlichkeiten zu. Die Grenze ist nicht willkürlich – sie folgt einem Prinzip: KI interpretiert und schlägt vor; deterministischer Code entscheidet und zeichnet auf.
KI-generierte Risikobewertungen und extrahierte Verpflichtungen sollten von qualifizierten Prüfern validiert werden, bevor sie für rechtliche oder kommerzielle Entscheidungen verwendet werden.
| Verantwortlichkeit | Gehandhabt von | Warum |
|---|---|---|
| Verstehen einer natürlichsprachlichen Anweisung | KI-Agent | Der ganze Sinn von Sprachmodellen |
| Extrahieren von Informationen aus unstrukturiertem Text | KI-Agent | Semantisches Verständnis erforderlich |
| Kategorisieren von Änderungen als substanziell oder formatierend | KI-Agent | Erfordert Urteilsvermögen über die Bedeutung |
| Vorschlagen von Genehmigern basierend auf Vertragstyp | Geschäftsregeln | Auditierbarkeit und Konsistenz |
| Bestimmen, wer unterzeichnen muss | Geschäftsregeln | Auditierbarkeit und Konsistenz |
| Aufzeichnen der Genehmigungskette | Deterministischer Code | Rechtlicher Prüfpfad muss manipulationssicher sein |
| Festlegen von Risikoschwellenwerten | Geschäftsregeln | Risikopolitik ist eine Governance-Entscheidung |
| Speichern von Verpflichtungsmetadaten | Deterministischer Code | Nachgelagerte Systeme benötigen zuverlässige Struktur |
| Auslösen von Verlängerungserinnerungen | Deterministischer Code | Muss planmäßig ausgelöst werden, nicht auf Inferenz basierend |
Die Risikoschwellenwerte in der Überprüfungsphase (AutoApproveThreshold = 0.3, ManualReviewThreshold = 0.6) werden vom Unternehmen festgelegt, nicht von der KI. Die Anwendung leitet ein Überprüfungsverhältnis aus der Anzahl der markierten Klauseln ab; das Unternehmen definiert die Schwellenwerte. Diese Trennung macht das System in einem Audit verteidigungsfähig – jede automatisierte Entscheidung lässt sich auf eine menschlich definierte Regel zurückführen, nicht auf eine Modellinferenz.
Integration in bestehende Systeme
Ein Vertragslebenszyklus-System existiert nicht isoliert. Die Metadaten nach der Unterzeichnung (Verpflichtungen, Schlüsseldaten, Verlängerungsbedingungen) sind dafür ausgelegt, von nachgelagerten Systemen konsumiert zu werden:
- ERP / Finanzen: Zahlungsmeilensteine und Rechnungsabstimmung
- CRM: Verlängerungswarnungen für Account-Manager
- Beschaffung: Lieferantenleistungsverfolgung gegen Vertrags-SLAs
- Rechtsabteilung: Compliance-Überwachung und Audit-Vorbereitung
Das JSON-Metadatenformat aus Phase 5 macht diese Integration unkompliziert – nachgelagerte Systeme fragen die strukturierten Daten ab, ohne Vertragsdokumente parsen zu müssen.
10. FAQ
Wie unterscheidet sich dies von der KI-Vertragsprüfung?
Die Vertragsprüfung ist eine Phase des Lebenszyklus – das Lesen einer Vereinbarung und das Markieren riskanter Klauseln. Ein Vertragsmanagementsystem deckt den gesamten Lebenszyklus ab: Entwurf, Überprüfung, Verhandlung, Genehmigung, Finalisierung und Überwachung nach der Unterzeichnung. Der bestehende Artikel KI-Vertragsprüfung in C# behandelt die Überprüfungsphase ausführlich; dieser Artikel behandelt die vollständige Pipeline, in die die Überprüfung eingebettet ist.
Kann die Pipeline sowohl PDF- als auch Word-Eingaben verarbeiten?
Ja, in Phasen, die Format-Dispatch implementieren. Die Überprüfungs- und Überwachungsphasen enthalten einen Switch auf die Dateierweiterung, der .docx an Document und .pdf an PdfDocument weiterleitet. Die Entwurfsphase erfordert eine .docx-Vorlage, und die Verhandlungsphase arbeitet mit Word-Dokumenten (Dokumentenvergleich verwendet Spire.Doc.Document). Dies spiegelt ein reales Muster wider: Format-Dispatch wird dort angewendet, wo PDFs von Gegenparteien erwartet werden, nicht einheitlich über jede Phase hinweg.
Was passiert, wenn ein Vertrag in einem Stapel fehlschlägt?
Der Stapelprozessor umschließt jeden Vertrag mit einem eigenen try-catch-Block. Ein Fehler in einem Vertrag wird mit der Fehlermeldung und der Phase, in der er auftrat, aufgezeichnet, und der Stapel fährt mit dem nächsten Vertrag fort. Die Stapelzusammenfassung verwendet die Restmethode zur Statuserhaltung: Flagged = Total - Successful - Errored, wodurch sichergestellt wird, dass jedes Ergebnis in genau einem Berichts-Bucket landet.
Entscheidet der KI-Agent, wer einen Vertrag genehmigt?
Nein. Die Genehmigungs-Routing-Regeln sind deterministische Geschäftslogik – in diesem Beispiel erfordern Verträge über 100.000 $ die Genehmigung des VP, über 500.000 $ die Genehmigung des CFO, und Lieferantenverträge benötigen die Zustimmung der Beschaffung. Der KI-Agent generiert die Genehmigungszusammenfassung, die die Genehmiger lesen, aber das Routing selbst ist Code. Dies hält die Genehmigungskette auditierbar und konsistent.
Wie funktioniert die Verhandlungsphase?
Die Verhandlungsphase nutzt zwei Funktionen aus der Spire.Doc-Dokumentschicht: Document.Compare(), um ein Diff zwischen Versionen zu generieren, und Document.TrackChanges, um die Revisionverfolgung zu aktivieren. Der KI-Agent analysiert dann das Vergleichsdokument und kategorisiert jede Änderung als substanziell (Klauseltext, Zahlen, Daten) oder formatierend (Stil, Abstand). Diese Kombination – deterministischer Vergleich plus KI-Kategorisierung – macht die Verhandlungsphase für Rechtsteams nützlich.
Was ist die Überwachung nach der Unterzeichnung?
Nachdem ein Vertrag finalisiert wurde (in der Produktion unterzeichnet), extrahiert das System Verpflichtungen (wer muss was bis wann tun), Schlüsseldaten (Verlängerung, Kündigung, Meilensteine) und Verlängerungsbedingungen (automatische Verlängerung, Kündigungsfrist) aus dem finalisierten Dokument. Diese Metadaten werden als strukturiertes JSON gespeichert, das nachgelagerte Systeme – ERP, CRM, Beschaffung – abfragen können, ohne das Vertragsdokument zu öffnen. Dies ist die Phase, die die meisten Vertragsprüfungstools nicht abdecken, und hier geht in manuellen Prozessen der meiste Wert verloren: Verpflichtungen vergessen, Verlängerungen verpasst, Fristen ohne Benachrichtigung überschritten.
Welche .NET-Abhängigkeiten sind erforderlich?
Das Spire.Agent.Office-NuGet-Paket, das transitiv Spire.Doc, Spire.Pdf, Spire.XLS und Spire.Presentation einbindet. Das Beispiel zielt auf .NET 6 oder höher ab; prüfen Sie vor der Bereitstellung die unterstützten Zielframeworks der Paketversion. Ein SpireToken (API-Schlüssel) ist erforderlich, damit der KI-Agent mit dem Sprachmodell-Service kommunizieren kann.
Bereit, Ihren Vertragslebenszyklus zu automatisieren?
Wenn Ihre Anwendung Vereinbarungen entwirft, überprüft oder verfolgt, verwandelt ein Dokument-KI-Agent eine natürlichsprachliche Anweisung in eine echte, formatierte Datei, anstatt in eine Extraktions- und Rekonstruktionspipeline, die Sie warten müssen. Befolgen Sie den Erste-Schritte-Leitfaden, um das SDK in ein .NET-Projekt einzubinden und Ihre erste Anweisung auszuführen, und verwenden Sie dann die fünf Phasenmuster oben für Ihre eigenen Vertragstypen, Genehmigungsregeln und Verpflichtungsverfolgung.
Weiterführende Literatur
- Batch-Vertragsgenerierung mit Spire.Agent.Office – die Entwurfsphase im Detail: Serienbrief und Platzhalterersetzung über Dutzende von Vorlagen gleichzeitig
- Rechnungsverarbeitung mit einem KI-Agenten in .NET automatisieren – dasselbe Extract-Validate-Review-Muster angewendet auf Rechnungen, bei denen jedes Dokument einmalig konsumiert wird
- Intelligente Dokumentenverarbeitung in .NET: Aufbau von IDP-Pipelines – Orchestrierung auf Pipeline-Ebene: Nebenläufigkeit, Fehlerisolierung pro Dokument und Stapelberichterstattung
- Spire.Agent.Office Produktübersicht – KI-Agent-SDKs für jedes Office-Dokumentformat