FDE PulseViệc làm FDE đang mở 314Mới đăng 7 ngày qua 10Chủ đề nổi bật: Đào tạo kỹ năng FDE tại Đông Nam Á

Tờ báo của nghề Forward Deployed Engineer

Bách khoa

Chấm quỹ đạo gọi tool của agent trên Langfuse: từ bộ dữ liệu vàng đến cổng chặn trong CI

Agent trả lời đúng vẫn có thể gọi thừa tool, bỏ cuộc giữa chừng hoặc đi sai đường. Bài này hướng dẫn bạn chấm cả con đường đó bằng một bộ dataset nhỏ và vài chục dòng Python.

Đồ hoạHai kiểu lỗi quỹ đạo qua precision và recall
Agent lãng phí: tra đơn hai lầnAgent bỏ cuộc sớm: chỉ tra đơn
Quỹ đạo thực tếlookup_order ×2, check_refund_policy, create_refundlookup_order rồi trả lời luôn
LCS với quỹ đạo vàng (3 bước)31
Precision = LCS / thực tế3/4 = 0,75 (thấp hơn)1/1 = 1
Recall = LCS / mong đợi3/3 = 11/3, khoảng 0,33 (thấp)
Hướng sửaPrompt hoặc bộ nhớ tốt hơn để không gọi lại toolĐiều kiện dừng chặt hơn

Cùng trả lời đúng hoặc gần đúng, nhưng precision và recall theo LCS tách được agent lãng phí khỏi agent bỏ cuộc sớm.

Đồ hoạ: FDE Times

Tóm tắt nhanh

  • Chấm đáp án cuối là black-box; chấm quỹ đạo cho biết agent hỏng ở bước nào.
  • Precision thấp, recall cao: agent gọi thừa. Precision cao, recall thấp: agent bỏ cuộc sớm.
  • Khi có nhiều đường đi hợp lệ, đừng khớp cứng mà dùng LLM-as-a-judge chấm theo rubric.
Chia sẻLinkedInFacebookX

Một agent hoàn tiền trả lời khách “Đơn của bạn đã được hoàn tiền”, và câu đó đúng. Có điều trên đường đi, nó đã tra cùng một đơn hàng hai lần.

Nếu chỉ chấm câu trả lời cuối, bạn sẽ không bao giờ thấy lần gọi thừa ấy. Lần này nó chỉ làm agent chậm hơn, nhưng nếu tool bị gọi lặp là một tool ghi dữ liệu, cái giá sẽ lớn hơn nhiều.

Tài liệu cookbook của Langfuse gọi kiểu chấm chỉ nhìn đáp án là black-box, vì nó bỏ qua mọi bước trung gian. Đánh giá quỹ đạo thì ngược lại: khi đáp án sai, nó chỉ ra lỗi xảy ra ở đúng bước nào trong quá trình suy luận.

Với một FDE, đây là khác biệt giữa câu “agent hơi kém” và câu “agent bỏ qua bước kiểm tra chính sách ở 3 trên 10 ca”.

Bài này dẫn bạn đi trọn một vòng. Bạn sẽ dựng một dataset có quỹ đạo vàng, viết các hàm chấm, chạy experiment trên Langfuse, đọc kết quả rồi gắn vào CI. Bạn cần Python, một tài khoản Langfuse và một agent bất kỳ có gọi tool. Ví dụ agent hoàn tiền dưới đây là tình huống giả định để minh họa.

Bước 1: Quỹ đạo vàng phải do người viết

Cookbook của Langfuse nói thẳng rằng bạn cần một bộ dữ liệu “gold standard” gồm input cùng output hoặc quỹ đạo mong đợi. Phần khó nhất của cả quy trình không nằm ở code. Khó nhất là ngồi với người vận hành nghiệp vụ và hỏi: với yêu cầu này, một nhân viên giỏi sẽ làm những bước nào, theo thứ tự nào?

Với agent hoàn tiền, giả sử quy trình đúng gồm ba bước: tra đơn, kiểm tra chính sách hoàn tiền, tạo lệnh hoàn tiền. Ghi lại thành một danh sách tên tool:

expected = ["lookup_order", "check_refund_policy", "create_refund"]

Kiểm tra: mỗi tool trong danh sách phải có tên trùng khớp từng ký tự với tên tool agent thực sự gọi. Nếu không, mọi điểm số phía sau đều sai.

Bước 2: Đưa quỹ đạo vào dataset Langfuse

Langfuse định nghĩa dataset là một tập input và expected output dùng để kiểm thử ứng dụng. Mỗi item có input, còn expected output là tùy chọn, và chính chỗ đó dùng để chứa quỹ đạo. Cookbook đặt chuỗi tool call mong đợi ở trường expected_output.trajectory.

Đoạn code dưới đây là phác thảo. Tên hàm và tham số có thể khác nhau giữa các phiên bản SDK, nên hãy đối chiếu với tài liệu của phiên bản bạn cài.

# Phác thảo — đối chiếu chữ ký hàm với tài liệu Langfuse SDK
langfuse.create_dataset_item(
    dataset_name="refund-agent-trajectory",
    input={"message": "Tôi muốn hoàn tiền đơn 1234"},
    expected_output={
        "trajectory": ["lookup_order", "check_refund_policy", "create_refund"]
    },
)

Kiểm tra: mở dataset trên giao diện Langfuse và xác nhận mỗi item hiển thị đủ cả input lẫn trajectory.

Bước 3: Ba kiểu khớp, ba câu hỏi khác nhau

Tài liệu đánh giá agent của Vertex AI (Google Cloud) mô tả ba kiểu khớp, và mỗi kiểu trả lời một câu hỏi riêng. Exact match yêu cầu đúng các tool call, đúng thứ tự, không thừa không thiếu.

In-order match chấp nhận tool call thừa, miễn đủ các tool tham chiếu theo đúng thứ tự. Any-order match chỉ cần đủ các tool tham chiếu, thứ tự không quan trọng.

def exact_match(actual, expected):
    return 1.0 if actual == expected else 0.0

def in_order_match(actual, expected):
    it = iter(actual)
    return 1.0 if all(tool in it for tool in expected) else 0.0

def any_order_match(actual, expected):
    # Đơn giản hóa: dùng set nên bỏ qua tool lặp lại
    return 1.0 if set(expected) <= set(actual) else 0.0

Giờ áp vào ca agent tra đơn hai lần: actual = ["lookup_order", "lookup_order", "check_refund_policy", "create_refund"]. Exact match cho 0 vì quỹ đạo không giống hệt chuẩn. In-order match cho 1 vì các bước bắt buộc vẫn đúng thứ tự, và any-order match cũng cho 1 vì không thiếu bước nào.

Cùng một lần chạy mà nhận một điểm 0 và hai điểm 1. Điểm nhị phân cho biết có lệch hay không, nhưng không cho biết lệch nhiều đến đâu. Đó là lý do cần thêm một thước đo liên tục.

Bước 4: Precision và recall theo LCS

Pydantic Evals tính hai chỉ số dựa trên chuỗi con chung dài nhất (LCS) giữa quỹ đạo thực tế và quỹ đạo mong đợi: precision bằng LCS chia độ dài quỹ đạo thực tế, recall bằng LCS chia độ dài quỹ đạo mong đợi. Pydantic cũng quy định khi cả hai quỹ đạo đều rỗng thì mọi chế độ đều được 1.0.

def lcs_len(a, b):
    dp = [[0] * (len(b) + 1) for _ in range(len(a) + 1)]
    for i in range(len(a)):
        for j in range(len(b)):
            dp[i+1][j+1] = dp[i][j] + 1 if a[i] == b[j] else max(dp[i][j+1], dp[i+1][j])
    return dp[-1][-1]

def precision_recall(actual, expected):
    if not actual and not expected:
        return 1.0, 1.0
    l = lcs_len(actual, expected)
    p = l / len(actual) if actual else 0.0   # quy ước riêng cho ca một bên rỗng
    r = l / len(expected) if expected else 0.0
    return p, r

Tính tay để chắc mình hiểu. Ca tra đơn hai lần có LCS bằng 3 và quỹ đạo thực tế dài 4, nên precision = 3/4 = 0,75 và recall = 3/3 = 1. Ca thứ hai: agent chỉ gọi lookup_order rồi trả lời luôn. LCS bằng 1, precision = 1/1 = 1, còn recall = 1/3, khoảng 0,33.

Decagon đưa ra một cách đọc rất gọn: precision thấp mà recall cao là agent lãng phí, precision cao mà recall thấp là agent bỏ cuộc sớm. Hai ca trên rơi đúng vào hai kiểu đó, và mỗi kiểu cần một cách sửa riêng.

Agent lãng phí thường cần prompt hoặc bộ nhớ tốt hơn để không gọi lại tool. Agent bỏ cuộc sớm thường cần điều kiện dừng chặt hơn.

Bước 5: Chạy experiment trên dataset có version

Langfuse cho phép chạy experiment trực tiếp trên dataset có version qua SDK, bằng run_experiment cùng một task function. Task function nhận một item, gọi agent và trả về danh sách tên tool agent đã gọi. Cách lấy danh sách đó tùy framework agent bạn dùng.

# Phác thảo — tên tham số cần đối chiếu tài liệu SDK
RECALLS = []  # gom recall từng item để dùng ở Bước 6

def task(item):
    return run_agent_and_collect_tool_names(item.input)  # hàm của bạn

def trajectory_eval(output, expected_output):
    p, r = precision_recall(output, expected_output["trajectory"])
    RECALLS.append(r)
    return {"precision": p, "recall": r}

dataset.run_experiment(name="baseline-v1", task=task, evaluators=[trajectory_eval])

Kiểm tra: mỗi item phải có một trace cùng điểm precision và recall. Hãy mở một trace có recall thấp và tìm đúng bước agent dừng lại. Đó chính là giá trị của glass-box.

Bước 6: Biến điểm số thành cổng chặn

Cookbook của Langfuse gợi ý đưa đánh giá vào CI/CD để chặn những lần triển khai làm tụt điểm trên benchmark dataset. Cách đơn giản nhất là lưu recall trung bình của lần chạy gần nhất vào một file, rồi cho build fail nếu lần chạy mới thấp hơn.

Script dưới đây là bản đơn giản hóa: nó lấy RECALLS gom được ở Bước 5, không đọc kết quả từ API của Langfuse. Gọi nó ngay sau run_experiment trong job CI.

# ci_gate.py — bản đơn giản hóa
import json, statistics, sys
from pathlib import Path

BASELINE = Path("eval/baseline_recall.json")

def gate(recalls):
    current = statistics.mean(recalls)
    if BASELINE.exists():
        previous = json.loads(BASELINE.read_text())["recall"]
        if current < previous:
            print(f"FAIL: recall giảm từ {previous:.2f} xuống {current:.2f}")
            sys.exit(1)
    BASELINE.parent.mkdir(parents=True, exist_ok=True)
    BASELINE.write_text(json.dumps({"recall": current}))
    print(f"OK: recall trung bình = {current:.2f}")

gate(RECALLS)

Hãy commit file baseline vào repo và chỉ cập nhật nó khi merge vào nhánh chính. Nếu để mọi nhánh tự ghi đè, một nhánh kém có thể hạ chuẩn cho nhánh sau. Nên gate thêm cả precision theo cùng cách, vì recall một mình không bắt được agent lãng phí.

Kiểm tra: cố tình làm hỏng agent, chẳng hạn sửa prompt để nó bỏ qua check_refund_policy. Recall của ca hoàn tiền tụt từ 1 xuống 2/3, recall trung bình giảm, và job CI phải kết thúc với mã lỗi 1. Nếu build vẫn xanh, cổng của bạn chưa hoạt động.

Với agent có quyền ghi như create_refund, nên cho cổng này chạy trên mọi thay đổi prompt, không chỉ trên thay đổi code.

Ba lỗi hay gặp

Lỗi đầu tiên là khớp cứng khi nghiệp vụ có nhiều đường đi hợp lệ. Nếu tra đơn bằng mã đơn hay bằng số điện thoại đều đúng, exact match sẽ phạt oan agent. Decagon khuyên dùng LLM-as-a-judge đọc toàn bộ trace theo một rubric cho những trường hợp như vậy, còn khớp cứng nên dành cho các quy trình có thứ tự bắt buộc.

Lỗi thứ hai là quên quyết định có tính tool call bị lỗi hay không. Pydantic có tùy chọn include_failed cho đúng câu hỏi này. Một lần gọi create_refund thất bại rồi thử lại sẽ cho precision rất khác tùy cách bạn chọn, vì vậy hãy chọn một lần rồi ghi rõ trong tài liệu.

Riêng với hàm any_order_match ở Bước 3, lần thử lại đó vô hình hoàn toàn. Hàm dùng set, nên create_refund xuất hiện một hay hai lần đều cho điểm 1. Muốn bắt tool lặp, hãy dựa vào precision theo LCS hoặc đổi sang so collections.Counter thay cho set.

Lỗi thứ ba là để dataset chỉ toàn ca “đường thẳng”. Hãy thêm những ca mà quỹ đạo đúng là rỗng, ví dụ khách chỉ chào hỏi; theo quy tắc của Pydantic, cả hai cùng rỗng thì được 1.0. Ca đáng lo là khi quỹ đạo mong đợi rỗng mà agent vẫn gọi, chẳng hạn, lookup_order:

Thước đo Điểm Vì sao
Exact match 0 Quỹ đạo thực tế khác danh sách rỗng
In-order match 1 Danh sách mong đợi rỗng nên điều kiện “đủ tool tham chiếu” luôn đúng
Any-order match 1 Cùng lý do: tập rỗng luôn nằm trong tập thực tế
Precision 0 LCS bằng 0, chia cho độ dài quỹ đạo thực tế
Recall 0 Quy ước riêng của hàm precision_recall ở trên, không phải của Pydantic

Vì vậy với ca quỹ đạo rỗng, hãy chấm bằng exact match hoặc precision, đừng tin in-order và any-order.

Ở phía khách hàng, kỹ năng này trông thế nào

Tại site khách hàng, buổi làm việc quan trọng nhất thường là buổi bạn ngồi với nhân viên vận hành để viết quỹ đạo vàng. Bộ dataset đó trở thành hợp đồng ngầm giữa bạn và khách về chuyện thế nào là “agent làm đúng”. Khi khách hỏi “bản mới có tốt hơn không?”, bạn trả lời bằng recall trước và sau, không phải bằng cảm giác.

Khi đọc JD của các vị trí FDE hay AI engineer, hãy để ý các cụm như “evaluation”, “eval harness”, “regression testing for agents”. Trong CV, đừng chỉ viết “xây dựng agent”.

Hãy viết rằng bạn dựng benchmark quỹ đạo cho agent, chẩn đoán được lỗi gọi thừa và lỗi bỏ cuộc sớm, rồi gắn cổng chặn hồi quy vào CI. Nếu có thể, ghi kèm recall trước và sau khi bạn sửa agent, đo trên chính dataset đó.

Agent sẽ ngày càng được giao nhiều quyền hơn trong hệ thống của khách. Người được tin để ký duyệt bản deploy tiếp theo sẽ là người chỉ ra được agent đã đi qua những bước nào.

5 nguồn
Đọc tiếp trên lộ trình · Chặng 6: Đo lườngHệ thống đã chạy nhưng không ai dùng: cách FDE đo và nâng adoptionDashboard báo xanh, pipeline không lỗi, nhưng người dùng của khách vẫn quay về bảng tính cũ. Đây là loại thất bại không có bug để sửa, và FDE vẫn phải tìm cho ra nguyên nhân.