בקש ממודל לסקור הסכם שירות והוא עושה עבודה סבירה: תנאי תשלום של תשעים יום, אחריות בלתי מוגבלת, סעיף העברת נתונים ללא אמצעי הגנה. התשובה מגיעה כרשימה בחלון צ׳אט.

לאחר מכן החוזה נשלח לעמית, שפותח אותו ורואה מסמך נקי. הסקירה נמצאת ביישום אחר, בשיחה שלא היה חלק ממנה, מקושרת למשפטים שעליו כעת למצוא ידנית.

המודל אינו הבעיה. אין לו דרך לכתוב לתוך הקובץ. פוסט זה עוסק במתן אפשרות כזו — בכ‑מאה שורות של C# ו-GroupDocs.Annotation for .NET, כך שהקוד עצמו מסמן PDF, Word, Excel ו‑PowerPoint בצד השרת, ללא תוסף Word וללא הרחבת דפדפן.

הדוגמה המלאה זמינה ב‑GitHub: ai-document-review-annotations-net.

שמרו על החוזה עם המודל קטן

אל תבקשו מהמודל קואורדינטות. בקשו ממנו טקסט והערה:

[
  {
    "quote": "pay all invoices within ninety days",
    "comment": "Ninety-day payment terms are three times our standard. Ask for thirty days.",
    "severity": "high"
  }
]

quote חייב להיות מועתק במדויק מהמסמך, מכיוון שהוא העוגן: הצעד הבא מחפש אותו בעמוד. ארבעה עד חמש עשרה מילים הוא האיזון המושלם — מספיק ארוך כדי להיות ייחודי, מספיק קצר כדי שלא יסתובב על חצי העמוד.

הכל נובע משלושת השדות הללו, ואין שום תלות בפורמט הקובץ. לכן פקודה אחת מספיקה ל‑PDF, ל‑.docx ול‑גיליון אלקטרוני.

מצאו את הציטוט בעמוד

GroupDocs.Annotation מחזיר את הטקסט של כל עמוד עם הגאומטריה שלו:

using Annotator annotator = new Annotator("service-agreement.docx");
IDocumentInfo info = annotator.Document.GetDocumentInfo();

foreach (PageInfo page in info.PagesInfo)
    foreach (TextLineInfo line in page.TextLines)
        Console.WriteLine($"{line.Text} @ {line.LeftIndent},{line.TopIndent} {line.Width}x{line.Height}");

הקריאה הזו עובדת עבור PDF, Word, Excel ו‑PowerPoint — מה שכלי ה‑Word‑only אינם יכולים לעשות.

התאמת הציטוט מתבצעת בשלושה ניסיונות, לפי הסדר: שורה אחת שמכילה אותו; רצף של שורות רצופות שהטקסט המשולב שלהן מכיל אותו, מכיוון שהמשפט בעמוד נשבר במקום שבו הציטוט של המודל אינו נשבר; ואם שניהם נכשלו, אין תוצאה. מציאה שלא ניתן לעגנה מדווחת, ולא מונחת על ניחוש. נרמז רווחים, מרכאות מסולסלות וקווים לפני ההשוואה — המודלים משחזרים טקסט עם טיפוגרפיה מסודרת יותר מהמסמך.

שני הכללים שמחליטים היכן יופיע הסימון

זה המקום שבו ניסיון ראשון לרוב טועה, ולכן כדאי לציין זאת במפורש.

מספרי העמודים נספרים פעמיים. PageInfo.PageNumber מ‑GetDocumentInfo() הוא מתחיל מ‑1. AnnotationBase.PageNumber הוא מתחיל מ‑0. העברת הערך הראשון לשני גורמת לכל הסימון לנחות עמוד אחד יותר מדי — והסימונים המיועדים לעמוד האחרון נעלמים, מכיוון שסימון לעמוד שאינו קיים נזרק בעת שמירת המסמך. ללא חריגה, ללא סימון, ללא הודעה.

שני מקורות. TextLineInfo ו‑AreaAnnotation.Box מודדים מהפינה עליונה‑שמאלית של העמוד. סימון טקסט — HighlightAnnotation.Points וקרוביו — מודדים מהפינה תחתונה‑שמאלית, הקונבנצייה שבה משתמש PDF עצמו. שורה ב‑TopIndent = t עם גובה h בעמוד בגובה H לכן משתרעת מ‑H - t - h עד H - t.

Member Origin Page numbering
PageInfo, TextLineInfo פינה עליונה‑שמאלית מתחיל מ‑1
AnnotationBase.PageNumber — מתחיל מ‑0
AreaAnnotation.Box פינה עליונה‑שמאלית —
HighlightAnnotation.Points פינה תחתונה‑שמאלית —
PreviewOptions.PageNumbers — מתחיל מ‑1

כתבו את הממצא במסמך

User reviewer = new User { Id = 1, Name = "AI reviewer", Role = Role.Editor };

float top    = (float)(page.Height - box.Y);
float bottom = (float)(page.Height - box.Y - box.Height);

annotator.Add(new HighlightAnnotation
{
    PageNumber = page.PageNumber - 1,
    BackgroundColor = 0xFFE699,
    Opacity = 0.5,
    Message = finding.Comment,
    CreatedOn = DateTime.Now,
    User = reviewer,
    Points = new List<Point>
    {
        new Point(left, top),  new Point(right, top),
        new Point(left, bottom), new Point(right, bottom)
    },
    Replies = new List<Reply>
    {
        new Reply
        {
            Comment = $"Severity: {finding.Severity}. Reply to accept or reject this remark.",
            RepliedOn = DateTime.Now,
            User = reviewer
        }
    }
});

annotator.Save("service-agreement.reviewed.docx");

פתחו את התוצאה ב‑Word והיא תיראה כמסמך סקירה רגיל: המשפט מודגש, ההערה במרווח, תגובה מתחתיו מחכה לתשובה. פתחו אותה ב‑Acrobat והיא תיראה כהדגשה עם פתק. שום דבר לא אומר “נוצר על‑ידי מכונה” חוץ משם המחבר — וזה המטרה.

שתי נקודות שכדאי לגנוב:

  • אל תחזרו על ההודעה בתגובה. Word מציג את שניהם, ולכן המשפט היה מופיע פעמיים במרווח. תנו להודעה לשאת את הממצא והתגובה תפתח את השרשרת.
  • הצבעים ב‑Word הם משוערים. BackgroundColor הוא 0xRRGGBB ו‑PDF שומר אותו בדיוק, אך ל‑Word יש פלטת צבעים קבועה של צבעי הדגשה והוא מצמיד את הצבע הקרוב ביותר. בחרו דרגות חומרה שנשארות מובחנות אחרי הצמדה.

החלק שהופך את זה לבטוח לנסות

כל סימון נושא מחבר, ולכן ניתן להסיר את כל החלק שנכתב על‑ידי מכונה בלי לגעת במילה שכתב אדם:

annotator.Remove(annotator.Get().Where(a => a.User?.Name == "AI reviewer").ToList());

הסקירה היא שכבה, לא עריכה. הריצו אותה על טיוטה, ראו מה היא מוצאת, מחקו אם מדובר ברעש, הריצו שוב עם פקודה משופרת. תגובות שהוסיפו אנשים נשארות באותן שרשורים ומוחזרות דרך Get(), כך שהמעבר השני יכול לראות מה התקבל ומה נדחה.

נסו זאת

git clone https://github.com/groupdocs-annotation/ai-document-review-annotations-net.git
cd ai-document-review-annotations-net
dotnet run --project src/AiReview -- review samples/service-agreement.docx

הדוגמה כוללת קובץ ממצאים ולכן היא פועלת ללא חיבור לאינטרנט; הגדירו ANTHROPIC_API_KEY והשתמשו ב‑llm-review כדי שהמודל ייצור אותם במקום. הפקודה היא קובץ markdown יחיד, והחלפת המודל היא החלפת קריאת HTTP אחת.

הגרסה המפורטת שלב‑אחר‑שלב, עם כללי הקואורדינטות, נמצאת בתיעוד: Write AI review comments into documents. אם אתם מעדיפים להפעיל זאת מלקוח צ׳אט במקום מקוד שלכם, ה‑GroupDocs.Annotation MCP server מבצע את אותה המשימה דרך Claude, Cursor או Copilot.